Merge remote-tracking branch 'upstream/master' into passlib

master
Alexander Graf 3 years ago
commit 2ba0d552e0

@ -13,7 +13,7 @@ on:
- '[1-9].[0-9].[0-9]'
# pre-releases, e.g. 1.8-pre1
- 1.8-pre[0-9]
# test branches, e.g. test-debian
# test branches, e.g. test-debian
- test-*
###############################################
@ -39,6 +39,21 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
#For branch TESTING, we set the image tag to PR-xxxx
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -58,7 +73,7 @@ jobs:
run: echo "$DOCKER_PW" | docker login --username $DOCKER_UN --password-stdin
- name: Build all docker images
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
run: docker-compose -f tests/build.yml build
@ -76,6 +91,20 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -92,9 +121,9 @@ jobs:
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test core suite
run: python tests/compose/test.py core 1
run: python tests/compose/test.py core 2
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
@ -109,6 +138,20 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -125,9 +168,9 @@ jobs:
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test fetch
run: python tests/compose/test.py fetchmail 1
run: python tests/compose/test.py fetchmail 2
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
@ -142,6 +185,20 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -158,9 +215,9 @@ jobs:
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test clamvav
run: python tests/compose/test.py filters 2
run: python tests/compose/test.py filters 3
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
@ -175,6 +232,20 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -191,9 +262,9 @@ jobs:
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test rainloop
run: python tests/compose/test.py rainloop 1
run: python tests/compose/test.py rainloop 2
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
@ -208,6 +279,20 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -224,9 +309,9 @@ jobs:
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test roundcube
run: python tests/compose/test.py roundcube 1
run: python tests/compose/test.py roundcube 2
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
@ -241,6 +326,20 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -257,9 +356,9 @@ jobs:
- name: Copy all certs
run: sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
- name: Test webdav
run: python tests/compose/test.py webdav 1
run: python tests/compose/test.py webdav 2
env:
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
@ -280,6 +379,21 @@ jobs:
shell: bash
run: |
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
#For branch TESTING, we set the image tag to PR-xxxx
- name: Derive MAILU_VERSION for branch testing
if: ${{ env.BRANCH == 'testing' }}
shell: bash
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "MAILU_VERSION=pr-${COMMIT_MESSAGE//[!0-9]/}" >> $GITHUB_ENV
- name: Derive MAILU_VERSION for other branches than testing
if: ${{ env.BRANCH != 'testing' }}
shell: bash
env:
MAILU_BRANCH: ${{ env.BRANCH }}
run: |
echo "MAILU_VERSION=${{ env.MAILU_BRANCH }}" >> $GITHUB_ENV
- name: Create folder for storing images
run: |
sudo mkdir -p /images
@ -300,9 +414,8 @@ jobs:
DOCKER_PW: ${{ secrets.Docker_Password }}
DOCKER_ORG: ${{ secrets.DOCKER_ORG }}
DOCKER_ORG_TESTS: ${{ secrets.DOCKER_ORG_TESTS }}
MAILU_VERSION: ${{ env.BRANCH }}
MAILU_VERSION: ${{ env.MAILU_VERSION }}
TRAVIS_BRANCH: ${{ env.BRANCH }}
TRAVIS_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: bash tests/deploy.sh
# This job is watched by bors. It only complets if building,testing and deploy worked.

@ -4,18 +4,49 @@ Changelog
Upgrade should run fine as long as you generate a new compose or stack
configuration and upgrade your mailu.env.
Please note that the current 1.8 is what we call a "soft release": Its there for everyone to see and use, but to limit possible user-impact of this very big release, its not yet the default in the setup-utility for new users. When upgrading, please treat it with some care, and be sure to always have backups!
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/.
See https://mailu.io/1.8/faq.html#how-can-i-override-settings for all the mappings.
Please note that the shipped image for PostgreSQL database is deprecated.
We advise to switch to an external database server.
One major change for the docker compose file is that the antispam container needs a fixed hostname [#1837](https://github.com/Mailu/Mailu/issues/1837).
This is handled when you regenerate the docker-compose file. A fixed hostname is required to retain rspamd history.
This is also handled in the helm-chart repo.
<!-- TOWNCRIER -->
v1.8.0 - 2020-09-28
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.
Please note that the shipped image for PostgreSQL database is deprecated.
We advise to switch to an external PostgreSQL database server.
1.8.0 - 2021-08-06
--------------------
- Features: Update version of roundcube webmail and carddav plugin. This is a security update. ([#1841](https://github.com/Mailu/Mailu/issues/1841))
- Features: Update version of rainloop webmail to 1.16.0. This is a security update. ([#1845](https://github.com/Mailu/Mailu/issues/1845))
- Features: Changed default value of AUTH_RATELIMIT_SUBNET to false. Increased default value of the rate limit in setup utility (AUTH_RATELIMIT) to a higher value. ([#1867](https://github.com/Mailu/Mailu/issues/1867))
- Features: Update jquery used in setup. Set pinned versions in requirements.txt for setup. This is a security update. ([#1880](https://github.com/Mailu/Mailu/issues/1880))
- Bugfixes: Replace PUBLIC_HOSTNAME and PUBLIC_IP in "Received" headers to ensure that no undue spam points are attributed ([#191](https://github.com/Mailu/Mailu/issues/191))
- Bugfixes: Don't replace nested headers (typically in attached emails) ([#1660](https://github.com/Mailu/Mailu/issues/1660))
- Bugfixes: Fix letsencrypt access to certbot for the mail-letsencrypt flavour ([#1686](https://github.com/Mailu/Mailu/issues/1686))
- Bugfixes: Fix CVE-2020-25275 and CVE-2020-24386 by upgrading alpine for
dovecot which contains a fixed dovecot version. ([#1720](https://github.com/Mailu/Mailu/issues/1720))
- Bugfixes: Antispam service now uses a static hostname. Rspamd history is only retained when the service has a fixed hostname. ([#1837](https://github.com/Mailu/Mailu/issues/1837))
- Bugfixes: Fix a bug preventing colons from being used in passwords when using radicale/webdav. ([#1861](https://github.com/Mailu/Mailu/issues/1861))
- Bugfixes: Remove dot in blueprint name to prevent critical flask startup error in setup. ([#1874](https://github.com/Mailu/Mailu/issues/1874))
- Bugfixes: fix punycode encoding of domain names ([#1891](https://github.com/Mailu/Mailu/issues/1891))
- Improved Documentation: Update fail2ban documentation to use systemd backend instead of filepath for journald ([#1857](https://github.com/Mailu/Mailu/issues/1857))
- Misc: 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. ([#1783](https://github.com/Mailu/Mailu/issues/1783))
v1.8.0rc - 2020-09-28
--------------------
- Features: Add support for backward-forwarding using SRS ([#328](https://github.com/Mailu/Mailu/issues/328))

@ -13,4 +13,4 @@ Before we can consider review and merge, please make sure the following list is
If an entry in not applicable, you can check it or remove it from the list.
- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/guide.html#changelog) entry file.
- [ ] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.

@ -1,8 +1,9 @@
# First stage to build assets
ARG DISTRO=alpine:3.12
ARG DISTRO=alpine:3.14
ARG ARCH=""
FROM ${ARCH}node:8 as assets
COPY --from=balenalib/rpi-alpine:3.10 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
FROM ${ARCH}node:16 as assets
COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
COPY package.json ./
RUN npm install
@ -24,9 +25,9 @@ RUN mkdir -p /app
WORKDIR /app
COPY requirements-prod.txt requirements.txt
RUN apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \
RUN apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \
&& apk add --no-cache --virtual build-dep \
libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev \
openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
&& pip3 install -r requirements.txt \
&& apk del --no-cache build-dep

@ -20,3 +20,4 @@
.sidebar-toggle {
padding: unset !important;
}

@ -1,10 +1,17 @@
require('./app.css');
import 'select2';
import 'admin-lte/plugins/select2/js/select2.js';
import 'admin-lte/plugins/datatables/jquery.dataTables.js';
import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.js';
import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.js';
import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.js';
jQuery("document").ready(function() {
jQuery(".mailselect").select2({
tags: true,
tokenSeparators: [',', ' ']
})
});
jQuery(".dataTable").DataTable({
"responsive": true,
});
});

@ -1,19 +1,22 @@
// jQuery
import jQuery from 'jquery';
import 'select2/dist/css/select2.css';
import 'admin-lte/plugins/select2/css/select2.css';
// bootstrap
import 'bootstrap/less/bootstrap.less';
import 'bootstrap';
// import 'bootstrap/less/bootstrap.less';
// import 'bootstrap';
// FA
import 'font-awesome/scss/font-awesome.scss';
// FontAwesome
import 'admin-lte/plugins/fontawesome-free/css/fontawesome.css';
import 'admin-lte/plugins/fontawesome-free/css/regular.css';
import 'admin-lte/plugins/fontawesome-free/css/solid.css';
// AdminLTE
import 'admin-lte/build/less/AdminLTE-without-plugins.less';
import 'admin-lte/build/less/select2.less';
import 'admin-lte/build/less/skins/skin-blue.less';
import 'admin-lte/build/scss/adminlte.scss';
import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.css';
import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.css';
import 'admin-lte/plugins/bootstrap/js/bootstrap.js';
import 'admin-lte/build/js/AdminLTE.js';
import 'admin-lte/build/js/Layout.js';
import 'admin-lte/build/js/ControlSidebar.js';
import 'admin-lte/build/js/PushMenu.js';
import 'admin-lte/build/js/BoxRefresh.js';

@ -32,10 +32,11 @@ DEFAULT_CONFIG = {
'DOMAIN': 'mailu.io',
'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io',
'POSTMASTER': 'postmaster',
'WILDCARD_SENDERS': '',
'TLS_FLAVOR': 'cert',
'INBOUND_TLS_ENFORCE': False,
'AUTH_RATELIMIT': '10/minute;1000/hour',
'AUTH_RATELIMIT_SUBNET': True,
'AUTH_RATELIMIT': '1000/minute;10000/hour',
'AUTH_RATELIMIT_SUBNET': False,
'DISABLE_STATISTICS': False,
# Mail settings
'DMARC_RUA': None,
@ -46,6 +47,7 @@ DEFAULT_CONFIG = {
'DKIM_SELECTOR': 'dkim',
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
'DEFAULT_QUOTA': 1000000000,
'MESSAGE_RATELIMIT': '200/day',
# Web settings
'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io',

@ -81,6 +81,13 @@ def handle_authentication(headers):
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
password = raw_password.encode("iso8859-1").decode("utf8")
ip = urllib.parse.unquote(headers["Client-Ip"])
service_port = int(urllib.parse.unquote(headers["Auth-Port"]))
if service_port == 25:
return {
"Auth-Status": "AUTH not supported",
"Auth-Error-Code": "502 5.5.1",
"Auth-Wait": 0
}
user = models.User.query.get(user_email)
if check_credentials(user, password, ip, protocol):
return {

@ -50,7 +50,7 @@ def user_authentication():
if (not flask_login.current_user.is_anonymous
and flask_login.current_user.enabled):
response = flask.Response()
response.headers["X-User"] = flask_login.current_user.get_id()
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, flask_login.current_user.get_id(), "")
response.headers["X-User-Token"] = models.User.get_temp_token(flask_login.current_user.get_id())
return response
return flask.abort(403)
@ -63,11 +63,11 @@ def basic_authentication():
authorization = flask.request.headers.get("Authorization")
if authorization and authorization.startswith("Basic "):
encoded = authorization.replace("Basic ", "")
user_email, password = base64.b64decode(encoded).split(b":")
user_email, password = base64.b64decode(encoded).split(b":", 1)
user = models.User.query.get(user_email.decode("utf8"))
if nginx.check_credentials(user, password.decode('utf-8'), flask.request.remote_addr, "web"):
response = flask.Response()
response.headers["X-User"] = user.email
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
return response
response = flask.Response(status=401)
response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'

@ -1,5 +1,6 @@
from mailu import models
from mailu import models, utils
from mailu.internal import internal
from flask import current_app as app
import flask
import idna
@ -31,7 +32,6 @@ def postfix_alias_map(alias):
destination = models.Email.resolve_destination(localpart, domain_name)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/transport/<path:email>")
def postfix_transport(email):
if email == '*' or re.match("(^|.*@)\[.*\]$", email):
@ -133,12 +133,20 @@ def postfix_sender_map(sender):
@internal.route("/postfix/sender/login/<path:sender>")
def postfix_sender_login(sender):
wildcard_senders = [s for s in flask.current_app.config.get('WILDCARD_SENDERS', '').lower().replace(' ', '').split(',') if s]
localpart, domain_name = models.Email.resolve_domain(sender)
if localpart is None:
return flask.abort(404)
return flask.jsonify(",".join(wildcard_senders)) if wildcard_senders else flask.abort(404)
destination = models.Email.resolve_destination(localpart, domain_name, True)
destination = [*destination, *wildcard_senders] if destination else [*wildcard_senders]
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/sender/rate/<path:sender>")
def postfix_sender_rate(sender):
""" Rate limit outbound emails per sender login
"""
user = models.User.get(sender) or flask.abort(404)
return flask.abort(404) if user.sender_limiter.hit() else flask.jsonify("450 4.2.1 You are sending too many emails too fast.")
@internal.route("/postfix/sender/access/<path:sender>")
def postfix_sender_access(sender):

@ -27,7 +27,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.inspection import inspect
from werkzeug.utils import cached_property
from mailu import dkim
from mailu import dkim, utils
db = flask_sqlalchemy.SQLAlchemy()
@ -57,10 +57,9 @@ class IdnaEmail(db.TypeDecorator):
def process_bind_param(self, value, dialect):
""" encode unicode domain part of email address to punycode """
localpart, domain_name = value.rsplit('@', 1)
localpart, domain_name = value.lower().rsplit('@', 1)
if '@' in localpart:
raise ValueError('email local part must not contain "@"')
domain_name = domain_name.lower()
return f'{localpart}@{idna.encode(domain_name).decode("ascii")}'
def process_result_value(self, value, dialect):
@ -272,11 +271,12 @@ class Domain(Base):
return dkim.strip_key(dkim_key).decode('utf8')
def generate_dkim_key(self):
""" generate and activate new DKIM key """
""" generate new DKIM key """
self.dkim_key = dkim.gen_key()
def has_email(self, localpart):
""" checks if localpart is configured for domain """
localpart = localpart.lower()
for email in chain(self.users, self.aliases):
if email.localpart == localpart:
return True
@ -355,8 +355,8 @@ class Email(object):
@email.setter
def email(self, value):
""" setter for email - sets _email, localpart and domain_name at once """
self.localpart, self.domain_name = value.rsplit('@', 1)
self._email = value
self._email = value.lower()
self.localpart, self.domain_name = self._email.rsplit('@', 1)
@staticmethod
def _update_localpart(target, value, *_):
@ -371,8 +371,8 @@ class Email(object):
@classmethod
def __declare_last__(cls):
# gets called after mappings are completed
sqlalchemy.event.listen(User.localpart, 'set', cls._update_localpart, propagate=True)
sqlalchemy.event.listen(User.domain_name, 'set', cls._update_domain_name, propagate=True)
sqlalchemy.event.listen(cls.localpart, 'set', cls._update_localpart, propagate=True)
sqlalchemy.event.listen(cls.domain_name, 'set', cls._update_domain_name, propagate=True)
def sendmail(self, subject, body):
""" send an email to the address """
@ -389,8 +389,7 @@ class Email(object):
def resolve_domain(cls, email):
""" resolves domain alternative to real domain """
localpart, domain_name = email.rsplit('@', 1) if '@' in email else (None, email)
alternative = Alternative.query.get(domain_name)
if alternative:
if alternative := Alternative.query.get(domain_name):
domain_name = alternative.domain_name
return (localpart, domain_name)
@ -401,12 +400,14 @@ class Email(object):
localpart_stripped = None
stripped_alias = None
if os.environ.get('RECIPIENT_DELIMITER') in localpart:
localpart_stripped = localpart.rsplit(os.environ.get('RECIPIENT_DELIMITER'), 1)[0]
delim = os.environ.get('RECIPIENT_DELIMITER')
if delim in localpart:
localpart_stripped = localpart.rsplit(delim, 1)[0]
user = User.query.get(f'{localpart}@{domain_name}')
if not user and localpart_stripped:
user = User.query.get(f'{localpart_stripped}@{domain_name}')
if user:
email = f'{localpart}@{domain_name}'
@ -416,15 +417,15 @@ class Email(object):
destination.append(email)
else:
destination = [email]
return destination
pure_alias = Alias.resolve(localpart, domain_name)
stripped_alias = Alias.resolve(localpart_stripped, domain_name)
if pure_alias and not pure_alias.wildcard:
return pure_alias.destination
if stripped_alias:
if stripped_alias := Alias.resolve(localpart_stripped, domain_name):
return stripped_alias.destination
if pure_alias:
@ -500,6 +501,12 @@ class User(Base, Email):
self.reply_enddate > now
)
@property
def sender_limiter(self):
return utils.limiter.get_limiter(
app.config["MESSAGE_RATELIMIT"], "sender", self.email
)
@classmethod
def get_password_context(cls):
""" create password context for hashing and verification

@ -1,29 +1,30 @@
# Translations template for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
# Translations template for Mailu.
# Copyright (C) 2018 Mailu
# This file is distributed under the same license as the Mailu project.
# Modi Sacks, 2019-2021.
# Yaron Shahrabani <sh.yaron@gmail.com>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Project-Id-Version: Mailu 1.5.1\n"
"Report-Msgid-Bugs-To: heb-bugzap@projects.hamakor.org.il \n"
"POT-Creation-Date: 2018-04-22 12:10+0200\n"
"PO-Revision-Date: 2019-11-27 22:20+0000\n"
"Last-Translator: Mordi Sacks \n"
"PO-Revision-Date: 2021-07-19 09:04+0300\n"
"Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n"
"Language-Team: Hebrew <https://translate.tedomum.net/projects/mailu/admin/he/"
">\n"
"Language: he\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
"n % 10 == 0) ? 2 : 3));\n"
"X-Generator: Weblate 3.3\n"
"X-Generator: Poedit 3.0\n"
"Generated-By: Babel 2.5.3\n"
#: mailu/ui/forms.py:32
msgid "Invalid email address."
msgstr "כתובת דוא\"ל לא חוקית."
msgstr "כתובת דוא״ל שגויה."
#: mailu/ui/forms.py:36
msgid "Confirm"
@ -31,7 +32,7 @@ msgstr "אישור"
#: mailu/ui/forms.py:40 mailu/ui/forms.py:77
msgid "E-mail"
msgstr "דוא\"ל"
msgstr "דוא״ל"
#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90
#: mailu/ui/forms.py:109 mailu/ui/forms.py:162
@ -48,23 +49,23 @@ msgstr "כניסה"
#: mailu/ui/templates/domain/details.html:27
#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17
msgid "Domain name"
msgstr "שם דומיין"
msgstr "שם תחום"
#: mailu/ui/forms.py:47
msgid "Maximum user count"
msgstr ""
msgstr "כמות המשתמשים המרבית"
#: mailu/ui/forms.py:48
msgid "Maximum alias count"
msgstr ""
msgstr "כמות הכינויים המרבית"
#: mailu/ui/forms.py:49
msgid "Maximum user quota"
msgstr ""
msgstr "מיכסת המשתמשים המרבית"
#: mailu/ui/forms.py:50
msgid "Enable sign-up"
msgstr ""
msgstr "לאפשר הרשמה"
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140
@ -72,53 +73,53 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19
#: mailu/ui/templates/user/list.html:23
msgid "Comment"
msgstr ""
msgstr "תגובה"
#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66
#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141
msgid "Create"
msgstr ""
msgstr "יצירה"
#: mailu/ui/forms.py:57
msgid "Initial admin"
msgstr ""
msgstr "מנהל ראשוני"
#: mailu/ui/forms.py:58
msgid "Admin password"
msgstr ""
msgstr "סיסמת ניהול"
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
msgid "Confirm password"
msgstr ""
msgstr "אישור סיסמה"
#: mailu/ui/forms.py:65
msgid "Alternative name"
msgstr ""
msgstr "שם חלופי"
#: mailu/ui/forms.py:70
msgid "Relayed domain name"
msgstr ""
msgstr "שם תחום מועבר"
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
msgid "Remote host"
msgstr ""
msgstr "מארח מרוחק"
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
#: mailu/ui/templates/user/signup_domain.html:16
msgid "Quota"
msgstr ""
msgstr "מיכסה"
#: mailu/ui/forms.py:81
msgid "Allow IMAP access"
msgstr ""
msgstr "לאפשר גישה ב־IMAP"
#: mailu/ui/forms.py:82
msgid "Allow POP3 access"
msgstr ""
msgstr "לאפשר גישה ב־POP3"
#: mailu/ui/forms.py:84
msgid "Enabled"
msgstr ""
msgstr "מופעל"
#: mailu/ui/forms.py:85
msgid "Save"
@ -126,7 +127,7 @@ msgstr "שמירה"
#: mailu/ui/forms.py:89
msgid "Email address"
msgstr "דואר אלקטרוני"
msgstr "כתובת דוא״ל"
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
#: mailu/ui/templates/user/signup.html:4
@ -136,244 +137,244 @@ msgstr "הרשמה"
#: mailu/ui/forms.py:97
msgid "Displayed name"
msgstr ""
msgstr "שם מוצג"
#: mailu/ui/forms.py:98
msgid "Enable spam filter"
msgstr ""
msgstr "הפעלת מסנן ספאם"
#: mailu/ui/forms.py:99
msgid "Spam filter tolerance"
msgstr ""
msgstr "סובלנות מסנן הספאם"
#: mailu/ui/forms.py:100
msgid "Enable forwarding"
msgstr ""
msgstr "הפעלת העברה"
#: mailu/ui/forms.py:101
msgid "Keep a copy of the emails"
msgstr ""
msgstr "להשאיר עותק מההודעות"
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
#: mailu/ui/templates/alias/list.html:20
msgid "Destination"
msgstr ""
msgstr "יעד"
#: mailu/ui/forms.py:105
msgid "Save settings"
msgstr ""
msgstr "שמירת הגדרות"
#: mailu/ui/forms.py:110
msgid "Password check"
msgstr ""
msgstr "בדיקת סיסמה"
#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16
msgid "Update password"
msgstr ""
msgstr "עדכון סיסמה"
#: mailu/ui/forms.py:115
msgid "Enable automatic reply"
msgstr ""
msgstr "הפעלת תגובה אוטומטית"
#: mailu/ui/forms.py:116
msgid "Reply subject"
msgstr ""
msgstr "נושא התגובה"
#: mailu/ui/forms.py:117
msgid "Reply body"
msgstr ""
msgstr "גוף התגובה"
#: mailu/ui/forms.py:119
msgid "End of vacation"
msgstr ""
msgstr "סוף החופשה"
#: mailu/ui/forms.py:120
msgid "Update"
msgstr ""
msgstr "עדכון"
#: mailu/ui/forms.py:125
msgid "Your token (write it down, as it will never be displayed again)"
msgstr ""
msgstr "האסימון שלך (כדאי לשמור עליו היטב כיוון שהוא לא יופיע פעם נוספת)"
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
msgid "Authorized IP"
msgstr ""
msgstr "כתובת IP מורשית"
#: mailu/ui/forms.py:136
msgid "Alias"
msgstr ""
msgstr "כינוי"
#: mailu/ui/forms.py:138
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
msgstr ""
msgstr "להשתמש בתחביר דמוי SQL (למשל: catch-all aliases)"
#: mailu/ui/forms.py:145
msgid "Admin email"
msgstr ""
msgstr "דוא״ל ההנהלה"
#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164
msgid "Submit"
msgstr ""
msgstr "הגשה"
#: mailu/ui/forms.py:150
msgid "Manager email"
msgstr ""
msgstr "דוא״ל המפקח"
#: mailu/ui/forms.py:155
msgid "Protocol"
msgstr ""
msgstr "פרוטוקול"
#: mailu/ui/forms.py:158
msgid "Hostname or IP"
msgstr ""
msgstr "שם מארח או כתובת IP"
#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20
#: mailu/ui/templates/client.html:47
msgid "TCP port"
msgstr ""
msgstr "פתחת TCP"
#: mailu/ui/forms.py:160
msgid "Enable TLS"
msgstr ""
msgstr "הפעלת TLS"
#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28
#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20
msgid "Username"
msgstr ""
msgstr "שם משתמש"
#: mailu/ui/forms.py:163
msgid "Keep emails on the server"
msgstr ""
msgstr "להשאיר את ההודעות על השרת"
#: mailu/ui/forms.py:168
msgid "Announcement subject"
msgstr ""
msgstr "נושא ההכרזה"
#: mailu/ui/forms.py:170
msgid "Announcement body"
msgstr ""
msgstr "גוף ההכרזה"
#: mailu/ui/forms.py:172
msgid "Send"
msgstr ""
msgstr "שליחה"
#: mailu/ui/templates/announcement.html:4
msgid "Public announcement"
msgstr ""
msgstr "הכרזה פומבית"
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
msgid "Client setup"
msgstr ""
msgstr "הגדרת לקוח"
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
msgid "Mail protocol"
msgstr ""
msgstr "פרוטוקול דוא״ל"
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
msgid "Server name"
msgstr ""
msgstr "שם שרת"
#: mailu/ui/templates/confirm.html:4
msgid "Confirm action"
msgstr ""
msgstr "אישור הפעולה"
#: mailu/ui/templates/confirm.html:13
#, python-format
msgid "You are about to %(action)s. Please confirm your action."
msgstr ""
msgstr "פעולה זו תבצע %(action)s. נא לאשר את הפעולה שלך."
#: mailu/ui/templates/docker-error.html:4
msgid "Docker error"
msgstr ""
msgstr "שגיאת Docker"
#: mailu/ui/templates/docker-error.html:12
msgid "An error occurred while talking to the Docker server."
msgstr ""
msgstr "אירעה שגיאה בעת החיבור לשרת ה־Docker."
#: mailu/ui/templates/login.html:8
msgid "to access the administration tools"
msgstr ""
msgstr "כדי לגשת לכלי הניהול"
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
msgid "Settings"
msgstr ""
msgstr "הגדרות"
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
msgid "Auto-reply"
msgstr ""
msgstr "מענה אוטומטית"
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
#: mailu/ui/templates/user/list.html:36
msgid "Fetched accounts"
msgstr ""
msgstr "חשבונות נמשכים"
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
msgid "Authentication tokens"
msgstr ""
msgstr "אסימוני אימות"
#: mailu/ui/templates/sidebar.html:35
msgid "Administration"
msgstr ""
msgstr "ניהול"
#: mailu/ui/templates/sidebar.html:44
msgid "Announcement"
msgstr ""
msgstr "הכרזה"
#: mailu/ui/templates/sidebar.html:49
msgid "Administrators"
msgstr ""
msgstr "מנהלים"
#: mailu/ui/templates/sidebar.html:54
msgid "Relayed domains"
msgstr ""
msgstr "שמות תחום מועברים"
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
msgid "Antispam"
msgstr ""
msgstr "מניעת ספאם"
#: mailu/ui/templates/sidebar.html:66
msgid "Mail domains"
msgstr ""
msgstr "דמות תחום לדוא״ל"
#: mailu/ui/templates/sidebar.html:72
msgid "Go to"
msgstr ""
msgstr "מעבר אל"
#: mailu/ui/templates/sidebar.html:76
msgid "Webmail"
msgstr ""
msgstr "דוא״ל בדפדפן"
#: mailu/ui/templates/sidebar.html:87
msgid "Website"
msgstr ""
msgstr "אתר"
#: mailu/ui/templates/sidebar.html:92
msgid "Help"
msgstr ""
msgstr "עזרה"
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
msgid "Register a domain"
msgstr ""
msgstr "רישום שם תחום"
#: mailu/ui/templates/sidebar.html:105
msgid "Sign out"
msgstr ""
msgstr "יציאה"
#: mailu/ui/templates/working.html:4
msgid "We are still working on this feature!"
msgstr ""
msgstr "אנחנו עדיין עובדים על היכולת הזאת!"
#: mailu/ui/templates/admin/create.html:4
msgid "Add a global administrator"
msgstr ""
msgstr "הוספת מנהל כללי"
#: mailu/ui/templates/admin/list.html:4
msgid "Global administrators"
msgstr ""
msgstr "מנהלים כלליים"
#: mailu/ui/templates/admin/list.html:9
msgid "Add administrator"
msgstr ""
msgstr "הוספת מנהל"
#: mailu/ui/templates/admin/list.html:16 mailu/ui/templates/alias/list.html:18
#: mailu/ui/templates/alternative/list.html:18
@ -382,12 +383,12 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:16 mailu/ui/templates/token/list.html:18
#: mailu/ui/templates/user/list.html:18
msgid "Actions"
msgstr ""
msgstr "פעולות"
#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19
#: mailu/ui/templates/manager/list.html:19 mailu/ui/templates/user/list.html:20
msgid "Email"
msgstr ""
msgstr "דוא״ל"
#: mailu/ui/templates/admin/list.html:22 mailu/ui/templates/alias/list.html:29
#: mailu/ui/templates/alternative/list.html:25
@ -396,23 +397,23 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:27 mailu/ui/templates/token/list.html:26
#: mailu/ui/templates/user/list.html:31
msgid "Delete"
msgstr ""
msgstr "מחיקה"
#: mailu/ui/templates/alias/create.html:4
msgid "Create alias"
msgstr ""
msgstr "יצירת כינוי"
#: mailu/ui/templates/alias/edit.html:4
msgid "Edit alias"
msgstr ""
msgstr "עריכת כינוי"
#: mailu/ui/templates/alias/list.html:4
msgid "Alias list"
msgstr ""
msgstr "רשימת כינויים"
#: mailu/ui/templates/alias/list.html:12
msgid "Add alias"
msgstr ""
msgstr "הוספת כינוי"
#: mailu/ui/templates/alias/list.html:22
#: mailu/ui/templates/alternative/list.html:20
@ -420,254 +421,259 @@ msgstr ""
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21
#: mailu/ui/templates/user/list.html:24
msgid "Created"
msgstr ""
msgstr "נוצר"
#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23
#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21
#: mailu/ui/templates/user/list.html:25
msgid "Last edit"
msgstr ""
msgstr "עריכה אחרונה"
#: mailu/ui/templates/alias/list.html:28 mailu/ui/templates/domain/list.html:30
#: mailu/ui/templates/fetch/list.html:30 mailu/ui/templates/relay/list.html:26
#: mailu/ui/templates/user/list.html:30
msgid "Edit"
msgstr ""
msgstr "עריכה"
#: mailu/ui/templates/alternative/create.html:4
msgid "Create alternative domain"
msgstr ""
msgstr "יצירת שם תחום חלופי"
#: mailu/ui/templates/alternative/list.html:4
msgid "Alternative domain list"
msgstr ""
msgstr "רשימת שמות תחום חלופיים"
#: mailu/ui/templates/alternative/list.html:12
msgid "Add alternative"
msgstr ""
msgstr "הוספת חלופה"
#: mailu/ui/templates/alternative/list.html:19
msgid "Name"
msgstr ""
msgstr "שם"
#: mailu/ui/templates/domain/create.html:4
#: mailu/ui/templates/domain/list.html:9
msgid "New domain"
msgstr ""
msgstr "שם תחום חדש"
#: mailu/ui/templates/domain/details.html:4
msgid "Domain details"
msgstr ""
msgstr "פרטי שם התחום"
#: mailu/ui/templates/domain/details.html:15
msgid "Regenerate keys"
msgstr ""
msgstr "יצירת מפתחות מחדש"
#: mailu/ui/templates/domain/details.html:17
msgid "Generate keys"
msgstr ""
msgstr "יצירת מפתחות"
#: mailu/ui/templates/domain/details.html:31
msgid "DNS MX entry"
msgstr ""
msgstr "רשומת MX ב־DNS"
#: mailu/ui/templates/domain/details.html:35
msgid "DNS SPF entries"
msgstr ""
msgstr "רשומות SPF ב־DNS"
#: mailu/ui/templates/domain/details.html:42
msgid "DKIM public key"
msgstr ""
msgstr "מפתח DKIM ציבורי"
#: mailu/ui/templates/domain/details.html:46
msgid "DNS DKIM entry"
msgstr ""
msgstr "רשומת DKIM ב־DNS"
#: mailu/ui/templates/domain/details.html:50
msgid "DNS DMARC entry"
msgstr ""
msgstr "רשומת DMARC ב־DNS"
#: mailu/ui/templates/domain/edit.html:4
msgid "Edit domain"
msgstr ""
msgstr "עריכת שם תחום"
#: mailu/ui/templates/domain/list.html:4
msgid "Domain list"
msgstr ""
msgstr "רשימת שמות תחום"
#: mailu/ui/templates/domain/list.html:17
msgid "Manage"
msgstr ""
msgstr "ניהול"
#: mailu/ui/templates/domain/list.html:19
msgid "Mailbox count"
msgstr ""
msgstr "כמות תיבות דוא״ל"
#: mailu/ui/templates/domain/list.html:20
msgid "Alias count"
msgstr ""
msgstr "כמות כינויים"
#: mailu/ui/templates/domain/list.html:28
msgid "Details"
msgstr ""
msgstr "פרטים"
#: mailu/ui/templates/domain/list.html:35
msgid "Users"
msgstr ""
msgstr "משתמשים"
#: mailu/ui/templates/domain/list.html:36
msgid "Aliases"
msgstr ""
msgstr "כינויים"
#: mailu/ui/templates/domain/list.html:37
msgid "Managers"
msgstr ""
msgstr "מפקחים"
#: mailu/ui/templates/domain/list.html:39
msgid "Alternatives"
msgstr ""
msgstr "חלופות"
#: mailu/ui/templates/domain/signup.html:13
msgid ""
"In order to register a new domain, you must first setup the\n"
" domain zone so that the domain <code>MX</code> points to this server"
msgstr ""
"כדי לרשום שם תחום חדש, תחילה עליך להקים את אזור התחום\n"
" (domain zone) כדי שה־<code>MX</code> של שם התחום יפנה לשרת הזה"
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n"
" please contact your DNS provider or administrator. Also, please wait "
"a\n"
" please contact your DNS provider or administrator. Also, please wait a\n"
" couple minutes after the <code>MX</code> is set so the local server "
"cache\n"
" expires."
msgstr ""
"אם לא ברור לך איך להקים רשומת <code>MX</code> עבור אזור ה־DNS שלך,\n"
" נא ליצור קשר עם ספק ה־ DNS או ההנהלה שלך. כמו כן, נא להמתין מספר דקות\n"
" לאחר הגדרת ה־<code>MX</code> כדי לאפשר לתוקף המטמון המקורי בשרת\n"
" לפוג."
#: mailu/ui/templates/fetch/create.html:4
msgid "Add a fetched account"
msgstr ""
msgstr "הוספת חשבון נמשך"
#: mailu/ui/templates/fetch/edit.html:4
msgid "Update a fetched account"
msgstr ""
msgstr "עדכון חשבון שנמשך"
#: mailu/ui/templates/fetch/list.html:12
msgid "Add an account"
msgstr ""
msgstr "הוספת חשבון"
#: mailu/ui/templates/fetch/list.html:19
msgid "Endpoint"
msgstr ""
msgstr "נקודת גישה"
#: mailu/ui/templates/fetch/list.html:21
msgid "Keep emails"
msgstr ""
msgstr "לשמור על ההודעות"
#: mailu/ui/templates/fetch/list.html:22
msgid "Last check"
msgstr ""
msgstr "בדיקה אחרונה"
#: mailu/ui/templates/fetch/list.html:35
msgid "yes"
msgstr ""
msgstr "כן"
#: mailu/ui/templates/fetch/list.html:35
msgid "no"
msgstr ""
msgstr "לא"
#: mailu/ui/templates/manager/create.html:4
msgid "Add a manager"
msgstr ""
msgstr "הוספת מנהל"
#: mailu/ui/templates/manager/list.html:4
msgid "Manager list"
msgstr ""
msgstr "רשימת מנהלים"
#: mailu/ui/templates/manager/list.html:12
msgid "Add manager"
msgstr ""
msgstr "הוספת מנהל"
#: mailu/ui/templates/relay/create.html:4
msgid "New relay domain"
msgstr ""
msgstr "שם תחום מועבר"
#: mailu/ui/templates/relay/edit.html:4
msgid "Edit relayd domain"
msgstr ""
msgstr "עריכת שמות תחום מועברים"
#: mailu/ui/templates/relay/list.html:4
msgid "Relayed domain list"
msgstr ""
msgstr "רשימת שמות תחום מועברים"
#: mailu/ui/templates/relay/list.html:9
msgid "New relayed domain"
msgstr ""
msgstr "שם תחום מועבר חדש"
#: mailu/ui/templates/token/create.html:4
msgid "Create an authentication token"
msgstr ""
msgstr "יצירת אסימון אימות"
#: mailu/ui/templates/token/list.html:12
msgid "New token"
msgstr ""
msgstr "אסימון חדש"
#: mailu/ui/templates/user/create.html:4
msgid "New user"
msgstr ""
msgstr "משתמש חדש"
#: mailu/ui/templates/user/create.html:15
msgid "General"
msgstr ""
msgstr "כללי"
#: mailu/ui/templates/user/create.html:22
msgid "Features and quotas"
msgstr ""
msgstr "יכולות ומיכסות"
#: mailu/ui/templates/user/edit.html:4
msgid "Edit user"
msgstr ""
msgstr "עריכת משתמש"
#: mailu/ui/templates/user/forward.html:4
msgid "Forward emails"
msgstr ""
msgstr "העברת הודעות"
#: mailu/ui/templates/user/list.html:4
msgid "User list"
msgstr ""
msgstr "רשימת משתמשים"
#: mailu/ui/templates/user/list.html:12
msgid "Add user"
msgstr ""
msgstr "הוספת משתמש"
#: mailu/ui/templates/user/list.html:19 mailu/ui/templates/user/settings.html:4
msgid "User settings"
msgstr ""
msgstr "הגדרות משתמש"
#: mailu/ui/templates/user/list.html:21
msgid "Features"
msgstr ""
msgstr "יכולות"
#: mailu/ui/templates/user/password.html:4
msgid "Password update"
msgstr ""
msgstr "עדכון סיסמה"
#: mailu/ui/templates/user/reply.html:4
msgid "Automatic reply"
msgstr ""
msgstr "מענה אוטומטי"
#: mailu/ui/templates/user/settings.html:22
msgid "Auto-forward"
msgstr ""
msgstr "העברה אוטומטית"
#: mailu/ui/templates/user/signup_domain.html:8
msgid "pick a domain for the new account"
msgstr ""
msgstr "נא לבחור שם תחום לחשבון החדש"
#: mailu/ui/templates/user/signup_domain.html:14
msgid "Domain"
msgstr ""
msgstr "שם תחום"
#: mailu/ui/templates/user/signup_domain.html:15
msgid "Available slots"
msgstr ""
msgstr "מקומות פנויים"

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.admin, class_='mailselect') }}

@ -5,24 +5,28 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.admin_create') }}">
<a class="btn btn-primary float-right" href="{{ url_for('.admin_create') }}">
{% trans %}Add administrator{% endtrans %}
</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
</tr>
{% for admin in admins %}
<tr>
<td>
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ admin }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for admin in admins %}
<tr>
<td>
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ admin }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -9,10 +9,10 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
{{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_field(form.wildcard) }}
{{ macros.form_field(form.destination, class_='mailselect') }}
{{ macros.form_field(form.comment) }}

@ -9,31 +9,35 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Destination{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
{% for alias in domain.aliases %}
<tr>
<td>
<a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.alias_delete', alias=alias.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ alias }}</td>
<td>{{ alias.destination|join(', ') or '-' }}</td>
<td>{{ alias.comment or '' }}</td>
<td>{{ alias.created_at }}</td>
<td>{{ alias.updated_at or '' }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Destination{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for alias in domain.aliases %}
<tr>
<td>
<a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.alias_delete', alias=alias.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ alias }}</td>
<td>{{ alias.destination|join(', ') or '-' }}</td>
<td>{{ alias.comment or '' }}</td>
<td>{{ alias.created_at }}</td>
<td>{{ alias.updated_at or '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -9,24 +9,28 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
</tr>
{% for alternative in domain.alternatives %}
<tr>
<td>
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ alternative }}</td>
<td>{{ alternative.created_at }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for alternative in domain.alternatives %}
<tr>
<td>
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ alternative }}</td>
<td>{{ alternative.created_at }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.announcement_subject) }}

@ -8,44 +8,58 @@
<link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}">
<title>Mailu-Admin - {{ config["SITENAME"] }}</title>
</head>
<body class="hold-transition skin-blue sidebar-mini">
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<header class="main-header">
<div class="logo">
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button">
<span class="sr-only">Toggle navigation</span>
</a>
<a href="{{ config["WEB_ADMIN"] }}">
<span class="logo-lg">{{ config["SITENAME"] }}</span>
</a>
</div>
</header>
<aside class="main-sidebar">
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">{{ session['language'] }}</a>
<div class="dropdown-menu dropdown-menu-right p-0">
{% for language in session['available_languages'] %}
<a class="dropdown-item {% if language == session['language'] %}active{% endif %} " href="{{ url_for('.set_language', language=language) }}">{{ language }}</a>
{% endfor %}
</div>
</li>
</ul>
</nav>
<aside class="main-sidebar sidebar-dark-primary">
<a href="{{ config["WEB_ADMIN"] }}" class="brand-link">
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span>
</a>
{% block sidebar %}
{% include "sidebar.html" %}
{% endblock %}
</aside>
<div class="content-wrapper">
<section class="content-header">
<div class="pull-right">
{% block main_action %}
{% endblock %}
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">{% block title %}{% endblock %}</h1>
<small>{% block subtitle %}{% endblock %}</small>
</div>
<div class="col-sm-6">
{% block main_action %}
{% endblock %}
</div>
</div>
</div>
<h1>
{% block title %}{% endblock %}
<small>{% block subtitle %}{% endblock %}</small>
</h1>
</section>
<section class="content">
<div class="content">
{{ utils.flashed_messages(container=False) }}
{% block content %}{% endblock %}
</section>
</div>
</div>
<footer class="main-footer">
Built with <i class="fa fa-heart"></i> using <a class="white-text" href="http://flask.pocoo.org/">Flask</a> and
<a class="white-text" href="https://almsaeedstudio.com/preview">AdminLTE</a>
<span class="pull-right"><i class="fa fa-code-fork"></i> on <a class="white-text" href="https://github.com/Mailu/Mailu">Github</a></a></span>
<a class="white-text" href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>
<span class="pull-right"><i class="fa fa-code-fork"></i>on <a class="white-text" href="https://github.com/Mailu/Mailu">Github</a></a></span>
</footer>
</div>
<script src="{{ url_for('.static', filename='vendor.js') }}"></script>

@ -1,3 +1,5 @@
<!--TODO add translations for: configure your client, Incoming mail and Outgoing mail-->
{% extends "base.html" %}
{% block title %}
@ -9,8 +11,7 @@ configure your email client
{% endblock %}
{% block content %}
{% call macros.box(title="Incoming mail") %}
<table class="table table-bordered">
{% call macros.table(title="Incoming mail", datatable=False) %}
<tbody>
<tr>
<th>{% trans %}Mail protocol{% endtrans %}</th>
@ -33,11 +34,9 @@ configure your email client
<td><pre>*******</pre></td>
</tr>
</tbody>
</table>
{% endcall %}
{% call macros.box(title="Outgoing mail") %}
<table class="table table-bordered">
{% call macros.table(title="Outgoing mail", datatable=False) %}
<tbody>
<tr>
<th>{% trans %}Mail protocol{% endtrans %}</th>
@ -60,6 +59,5 @@ configure your email client
<td><pre>*******</pre></td>
</tr>
</tbody>
</table>
{% endcall %}
{% endblock %}

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
{% call macros.box(theme="warning") %}
{% call macros.card(theme="warning") %}
<p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p>
{{ macros.form(form) }}
{% endcall %}

@ -5,13 +5,13 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.name) }}
{{ macros.form_fields((form.max_users, form.max_aliases)) }}
{{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000,
prepend='<span class="input-group-addon"><span id="quota">'+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+'</span> GiB</span>',
prepend='<span class="input-group-text"><span id="quota">'+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+'</span> GiB</span>',
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.signup_enabled) }}
{{ macros.form_field(form.comment) }}

@ -10,7 +10,7 @@
{% block main_action %}
{% if current_user.global_admin %}
<a class="btn btn-primary" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">
<a class="btn btn-primary float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">
{% if domain.dkim_publickey %}
{% trans %}Regenerate keys{% endtrans %}
{% else %}
@ -21,7 +21,7 @@
{% endblock %}
{% block content %}
{% call macros.table() %}
{% call macros.table(datatable=False) %}
{% set hostname = config["HOSTNAMES"].split(",")[0] %}
<tr>
<th>{% trans %}Domain name{% endtrans %}</th>

@ -6,46 +6,50 @@
{% block main_action %}
{% if current_user.global_admin %}
<a class="btn btn-primary" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a>
{% endif %}
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Manage{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Mailbox count{% endtrans %}</th>
<th>{% trans %}Alias count{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
{% for domain in current_user.get_managed_domains() %}
<tr>
<td>
<a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>&nbsp;
{% if current_user.global_admin %}
<a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
{% endif %}
</td>
<td>
<a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="fa fa-envelope-o"></i></a>&nbsp;
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp;
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp;
{% if current_user.global_admin %}
<a href="{{ url_for('.alternative_list', domain_name=domain.name) }}" title="{% trans %}Alternatives{% endtrans %}"><i class="fa fa-asterisk"></i></a>&nbsp;
{% endif %}
</td>
<td>{{ domain.name }}</td>
<td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
<td>{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td>
<td>{{ domain.comment or '' }}</td>
<td>{{ domain.created_at }}</td>
<td>{{ domain.updated_at or '' }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Manage{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Mailbox count{% endtrans %}</th>
<th>{% trans %}Alias count{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for domain in current_user.get_managed_domains() %}
<tr>
<td>
<a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>&nbsp;
{% if current_user.global_admin %}
<a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
{% endif %}
</td>
<td>
<a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="far fa-envelope"></i></a>&nbsp;
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp;
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp;
{% if current_user.global_admin %}
<a href="{{ url_for('.alternative_list', domain_name=domain.name) }}" title="{% trans %}Alternatives{% endtrans %}"><i class="fa fa-asterisk"></i></a>&nbsp;
{% endif %}
</td>
<td>{{ domain.name }}</td>
<td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
<td>{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td>
<td>{{ domain.comment or '' }}</td>
<td>{{ domain.created_at }}</td>
<td>{{ domain.updated_at or '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -9,7 +9,7 @@
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{% call macros.box(title="Requirements") %}
{% call macros.card(title="Requirements") %}
<p>{% trans %}In order to register a new domain, you must first setup the
domain zone so that the domain <code>MX</code> points to this server{% endtrans %}
(<code>{{ config["HOSTNAMES"].split(",")[0] }}</code>).
@ -22,9 +22,9 @@
</p>
{% endcall %}
{% call macros.box() %}
{% call macros.card() %}
{% if form.localpart %}
{{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-addon">@</span>') }}
{{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-text">@</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }}
{% else %}
{{ macros.form_field(form.name) }}

@ -11,18 +11,18 @@
{% block content %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{% call macros.box(title="Remote server") %}
{% call macros.card(title="Remote server") %}
{{ macros.form_field(form.protocol) }}
{{ macros.form_fields((form.host, form.port)) }}
{{ macros.form_field(form.tls) }}
{% endcall %}
{% call macros.box(title="Authentication") %}
{% call macros.card(title="Authentication") %}
{{ macros.form_field(form.username) }}
{{ macros.form_field(form.password) }}
{% endcall %}
{% call macros.box(title="Settings") %}
{% call macros.card(title="Settings") %}
{{ macros.form_field(form.keep) }}
{% endcall %}

@ -9,35 +9,39 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Endpoint{% endtrans %}</th>
<th>{% trans %}Username{% endtrans %}</th>
<th>{% trans %}Keep emails{% endtrans %}</th>
<th>{% trans %}Last check{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
{% for fetch in user.fetches %}
<tr>
<td>
<a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.fetch_delete', fetch_id=fetch.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td>
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{{ fetch.last_check or '-' }}</td>
<td>{{ fetch.error or '-' }}</td>
<td>{{ fetch.created_at }}</td>
<td>{{ fetch.updated_at or '' }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Endpoint{% endtrans %}</th>
<th>{% trans %}Username{% endtrans %}</th>
<th>{% trans %}Keep emails{% endtrans %}</th>
<th>{% trans %}Last check{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for fetch in user.fetches %}
<tr>
<td>
<a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.fetch_delete', fetch_id=fetch.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td>
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{{ fetch.last_check or '-' }}</td>
<td>{{ fetch.error or '-' }}</td>
<td>{{ fetch.created_at }}</td>
<td>{{ fetch.updated_at or '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
{{ macros.form(form) }}
{% endcall %}
{% endblock %}

@ -37,9 +37,10 @@
{{ field.label if label else '' }}
{% else %}
{{ field.label if label else '' }}{{ form_field_errors(field) }}
{% if prepend or append %}<div class="input-group">{% endif %}
{{ prepend|safe }}{{ field(class_="form-control " + class_, **kwargs) }}{{ append|safe }}
{% if prepend or append %}</div>{% endif %}
{% if prepend %}<div class="input-group-prepend">{% endif %}
{% if append %}<div class="input-group-append">{% endif %}
{{ prepend|safe }}{{ field(class_="form-control " + class_, **kwargs) }}{{ append|safe }}
{% if prepend or append %}</div>{% endif %}
{% endif %}
{% endmacro %}
@ -64,18 +65,18 @@
</form>
{% endmacro %}
{% macro box(title=None, theme="primary", header=True) %}
{% macro card(title=None, theme="primary", header=True) %}
<div class="row">
<div class="col-lg-12">
<div class="box box-{{ theme }}">
<div class="card card-outline card-{{ theme }}">
{% if header %}
<div class="box-header">
<div class="card-header border-0">
{% if title %}
<h3 class="box-title">{{ title }}</h3>
<h3 class="card-title">{{ title }}</h3>
{% endif %}
</div>
{% endif %}
<div class="box-body">
<div class="card-body">
{{ caller() }}
</div>
</div>
@ -83,15 +84,20 @@
</div>
{% endmacro %}
{% macro table(theme="primary") %}
{% macro table(title=None, theme="primary", datatable=True) %}
<div class="row">
<div class="col-lg-12">
<div class="box box-{{ theme }}">
<table class="table table-bordered">
<tbody>
<div class="card card-outline card-{{ theme }}">
<div class="card-header border-0">
{% if title %}
<h3 class="card-title">{{ title }}</h3>
{% endif %}
</div>
<div class="card-body">
<table class="table table-bordered {% if datatable %} dataTable {% endif %}">
{{ caller() }}
</tbody>
</table>
</table>
</div>
</div>
</div>
</div>

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.manager, class_='mailselect') }}

@ -9,15 +9,18 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.manager_create', domain_name=domain.name) }}">{% trans %}Add manager{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.manager_create', domain_name=domain.name) }}">{% trans %}Add manager{% endtrans %}</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
</tr>
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for manager in domain.managers %}
<tr>
<td>
@ -26,5 +29,6 @@
<td>{{ manager }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -6,32 +6,36 @@
{% block main_action %}
{% if current_user.global_admin %}
<a class="btn btn-primary" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a>
{% endif %}
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Remote host{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
{% for relay in relays %}
<tr>
<td>
<a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.relay_delete', relay_name=relay.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
</td>
<td>{{ relay.name }}</td>
<td>{{ relay.smtp or '-' }}</td>
<td>{{ relay.comment or '' }}</td>
<td>{{ relay.created_at }}</td>
<td>{{ relay.updated_at or '' }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Domain name{% endtrans %}</th>
<th>{% trans %}Remote host{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for relay in relays %}
<tr>
<td>
<a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.relay_delete', relay_name=relay.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
</td>
<td>{{ relay.name }}</td>
<td>{{ relay.smtp or '-' }}</td>
<td>{{ relay.comment or '' }}</td>
<td>{{ relay.created_at }}</td>
<td>{{ relay.updated_at or '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -1,120 +1,144 @@
<section class="sidebar">
<div class="sidebar">
{% if current_user.is_authenticated %}
<h4 class="text-center text-primary">{{ current_user }}</h4>
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="info">
<span class="text-center text-primary">{{ current_user }}</span>
</div>
</div>
{% endif %}
<ul class="sidebar-menu" data-widget="tree">
{% if current_user.is_authenticated %}
<li class="header">{% trans %}My account{% endtrans %}</li>
<li>
<a href="{{ url_for('.user_settings') }}">
<i class="fa fa-wrench"></i> <span>{% trans %}Settings{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ url_for('.user_password') }}">
<i class="fa fa-lock"></i> <span>{% trans %}Update password{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ url_for('.user_reply') }}">
<i class="fa fa-plane"></i> <span>{% trans %}Auto-reply{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ url_for('.fetch_list') }}">
<i class="fa fa-download"></i> <span>{% trans %}Fetched accounts{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ url_for('.token_list') }}">
<i class="fa fa-ticket"></i> <span>{% trans %}Authentication tokens{% endtrans %}</span>
</a>
</li>
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" role="menu">
{% if current_user.is_authenticated %}
<li class="nav-header">{% trans %}My account{% endtrans %}</li>
<li class="nav-item">
<a href="{{ url_for('.user_settings') }}" class="nav-link">
<i class="nav-icon fa fa-wrench"></i>
<p class="text">{% trans %}Settings{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.user_password') }}" class="nav-link">
<i class="nav-icon fa fa-lock"></i>
<p class="text">{% trans %}Update password{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.user_reply') }}" class="nav-link">
<i class="nav-icon fa fa-plane"></i>
<p class="text">{% trans %}Auto-reply{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.fetch_list') }}" class="nav-link">
<i class="nav-icon fas fa-download"></i>
<p class="text">{% trans %}Fetched accounts{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.token_list') }}" class="nav-link">
<i class="nav-icon fas fa-ticket-alt"></i>
<p class="text">{% trans %}Authentication tokens{% endtrans %}</p>
</a>
</li>
{% if current_user.manager_of or current_user.global_admin %}
<li class="header">{% trans %}Administration{% endtrans %}</li>
{% endif %}
{% if current_user.global_admin %}
<li>
<a href="{{ url_for('.announcement') }}">
<i class="fa fa-bullhorn"></i> <span>{% trans %}Announcement{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ url_for('.admin_list') }}">
<i class="fa fa-user"></i> <span>{% trans %}Administrators{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ url_for('.relay_list') }}">
<i class="fa fa-reply-all"></i> <span>{% trans %}Relayed domains{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ config["WEB_ADMIN"] }}/antispam/" target="_blank">
<i class="fa fa-trash-o"></i> <span>{% trans %}Antispam{% endtrans %}</span>
</a>
</li>
{% endif %}
{% if current_user.manager_of or current_user.global_admin %}
<li>
<a href="{{ url_for('.domain_list') }}">
<i class="fa fa-envelope"></i> <span>{% trans %}Mail domains{% endtrans %}</span>
</a>
</li>
{% endif %}
{% endif %}
{% if current_user.manager_of or current_user.global_admin %}
<li class="nav-header">{% trans %}Administration{% endtrans %}</li>
{% endif %}
{% if current_user.global_admin %}
<li class="nav-item">
<a href="{{ url_for('.announcement') }}" class="nav-link">
<i class="nav-icon fa fa-bullhorn"></i>
<p class="text">{% trans %}Announcement{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.admin_list') }}" class="nav-link">
<i class="nav-icon fa fa-user"></i>
<p class="text">{% trans %}Administrators{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.relay_list') }}" class="nav-link">
<i class="nav-icon fa fa-reply-all"></i>
<p class="text">{% trans %}Relayed domains{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ config["WEB_ADMIN"] }}/antispam/" target="_blank" class="nav-link">
<i class="nav-icon fas fa-trash-alt"></i>
<p class="text">{% trans %}Antispam{% endtrans %}</p>
</a>
</li>
{% endif %}
{% if current_user.manager_of or current_user.global_admin %}
<li class="nav-item">
<a href="{{ url_for('.domain_list') }}" class="nav-link">
<i class="nav-icon fa fa-envelope"></i>
<p class="text">{% trans %}Mail domains{% endtrans %}</p>
</a>
</li>
{% endif %}
{% endif %}
<li class="header">{% trans %}Go to{% endtrans %}</li>
{% if config["WEBMAIL"] != "none" %}
<li>
<a href="{{ config["WEB_WEBMAIL"] }}" target="_blank">
<i class="fa fa-envelope-o"></i> <span>{% trans %}Webmail{% endtrans %}</span>
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('.client') }}">
<i class="fa fa-laptop"></i> <span>{% trans %}Client setup{% endtrans %}</span>
</a>
</li>
<li>
<a href="{{ config["WEBSITE"] }}" target="_blank">
<i class="fa fa-globe"></i> <span>{% trans %}Website{% endtrans %}</span>
</a>
</li>
<li>
<a href="https://mailu.io" target="_blank">
<i class="fa fa-life-ring"></i> <span>{% trans %}Help{% endtrans %}</span>
</a>
</li>
{% if config['DOMAIN_REGISTRATION'] %}
<li>
<a href="{{ url_for('.domain_signup') }}">
<i class="fa fa-plus-square"></i> <span>{% trans %}Register a domain{% endtrans %}</span>
</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li>
<a href="{{ url_for('.logout') }}">
<i class="fa fa-sign-out"></i> <span>{% trans %}Sign out{% endtrans %}</span>
</a>
</li>
{% else %}
<li>
<a href="{{ url_for('.login') }}">
<i class="fa fa-sign-in"></i> <span>{% trans %}Sign in{% endtrans %}</span>
</a>
</li>
{% if signup_domains %}
<li>
<a href="{{ url_for('.user_signup') }}">
<i class="fa fa-user-plus"></i> <span>{% trans %}Sign up{% endtrans %}</span>
</a>
</li>
{% endif %}
{% endif %}
</ul>
</section>
<li class="nav-header">{% trans %}Go to{% endtrans %}</li>
{% if config["WEBMAIL"] != "none" %}
<li class="nav-item">
<a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link">
<i class="nav-icon far fa-envelope"></i>
<p class="text">{% trans %}Webmail{% endtrans %}</p>
</a>
</li>
{% endif %}
<li class="nav-item">
<a href="{{ url_for('.client') }}" class="nav-link">
<i class="nav-icon fa fa-laptop"></i>
<p class="text">{% trans %}Client setup{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link">
<i class="nav-icon fa fa-globe"></i>
<p class="text">{% trans %}Website{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="https://mailu.io" target="_blank" class="nav-link">
<i class="nav-icon fa fa-life-ring"></i>
<p class="text">{% trans %}Help{% endtrans %}</p>
</a>
</li>
{% if config['DOMAIN_REGISTRATION'] %}
<li class="nav-item">
<a href="{{ url_for('.domain_signup') }}" class="nav-link">
<i class="nav-icon fa fa-plus-square"></i>
<p class="text">{% trans %}Register a domain{% endtrans %}</p>
</a>
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="nav-item">
<a href="{{ url_for('.logout') }}" class="nav-link">
<i class="nav-icon fas fa-sign-out-alt"></i>
<p class="text">{% trans %}Sign out{% endtrans %}</p>
</a>
</li>
{% else %}
<li class="nav-item">
<a href="{{ url_for('.login') }}" class="nav-link">
<i class="nav-icon fas fa-sign-in-alt"></i>
<p class="text">{% trans %}Sign in{% endtrans %}</p>
</a>
</li>
{% if signup_domains %}
<li class="nav-item">
<a href="{{ url_for('.user_signup') }}" class="nav-link">
<i class="nav-icon fa fa-user-plus"></i>
<p class="text">{% trans %}Sign up{% endtrans %}</p>
</a>
</li>
{% endif %}
{% endif %}
</ul>
</nav>
</div>

@ -9,26 +9,30 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Authorized IP{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
</tr>
{% for token in user.tokens %}
<tr>
<td>
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ token.comment }}</td>
<td>{{ token.ip or "any" }}</td>
<td>{{ token.created_at }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Authorized IP{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for token in user.tokens %}
<tr>
<td>
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ token.comment }}</td>
<td>{{ token.ip or "any" }}</td>
<td>{{ token.created_at }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -12,17 +12,17 @@
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{% call macros.box(_("General")) %}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
{% call macros.card(_("General")) %}
{{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }}
{{ macros.form_field(form.displayed_name) }}
{{ macros.form_field(form.comment) }}
{{ macros.form_field(form.enabled) }}
{% endcall %}
{% call macros.box(_("Features and quotas"), theme="success") %}
{% call macros.card(_("Features and quotas"), theme="success") %}
{{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50000000000),
prepend='<span class="input-group-addon"><span id="quota">'+((form.quota_bytes.data//1000000000).__str__() if form.quota_bytes.data else '∞')+'</span> GiB</span>',
prepend='<span class="input-group-text"><span id="quota">'+((form.quota_bytes.data//1000000000).__str__() if form.quota_bytes.data else '∞')+'</span> GiB</span>',
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.enable_imap) }}
{{ macros.form_field(form.enable_pop) }}

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.forward_enabled,

@ -9,42 +9,46 @@
{% endblock %}
{% block main_action %}
<a class="btn btn-primary" href="{{ url_for('.user_create', domain_name=domain.name) }}">{% trans %}Add user{% endtrans %}</a>
<a class="btn btn-primary float-right" href="{{ url_for('.user_create', domain_name=domain.name) }}">{% trans %}Add user{% endtrans %}</a>
{% endblock %}
{% block content %}
{% call macros.table() %}
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}User settings{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Features{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
{% for user in domain.users %}
<tr {% if not user.enabled %}class="warning"{% endif %}>
<td>
<a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
<a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>
<a href="{{ url_for('.user_settings', user_email=user.email) }}" title="{% trans %}Settings{% endtrans %}"><i class="fa fa-wrench"></i></a>&nbsp;
<a href="{{ url_for('.user_reply', user_email=user.email) }}" title="{% trans %}Auto-reply{% endtrans %}"><i class="fa fa-plane"></i></a>&nbsp;
<a href="{{ url_for('.fetch_list', user_email=user.email) }}" title="{% trans %}Fetched accounts{% endtrans %}"><i class="fa fa-download"></i></a>&nbsp;
</td>
<td>{{ user }}</td>
<td>
{% if user.enable_imap %}<span class="label label-info">imap</span>{% endif %}
{% if user.enable_pop %}<span class="label label-info">pop3</span>{% endif %}
</td>
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
<td>{{ user.comment or '-' }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at or '' }}</td>
</tr>
{% endfor %}
<thead>
<tr>
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}User settings{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Features{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for user in domain.users %}
<tr {% if not user.enabled %}class="warning"{% endif %}>
<td>
<a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>
<a href="{{ url_for('.user_settings', user_email=user.email) }}" title="{% trans %}Settings{% endtrans %}"><i class="fa fa-wrench"></i></a>&nbsp;
<a href="{{ url_for('.user_reply', user_email=user.email) }}" title="{% trans %}Auto-reply{% endtrans %}"><i class="fa fa-plane"></i></a>&nbsp;
<a href="{{ url_for('.fetch_list', user_email=user.email) }}" title="{% trans %}Fetched accounts{% endtrans %}"><i class="fa fa-download"></i></a>&nbsp;
</td>
<td>{{ user }}</td>
<td>
{% if user.enable_imap %}<span class="badge bg-info">imap</span>{% endif %}
{% if user.enable_pop %}<span class="badge bg-info">pop3</span>{% endif %}
</td>
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
<td>{{ user.comment or '-' }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at or '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endcall %}
{% endblock %}

@ -9,12 +9,12 @@
{% endblock %}
{% block content %}
{% call macros.box() %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ 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,

@ -12,18 +12,18 @@
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{% call macros.box(title=_("Displayed name")) %}
{% call macros.card(title=_("Displayed name")) %}
{{ macros.form_field(form.displayed_name) }}
{% endcall %}
{% call macros.box(title=_("Antispam")) %}
{% call macros.card(title=_("Antispam")) %}
{{ macros.form_field(form.spam_enabled) }}
{{ macros.form_field(form.spam_threshold, step=1, max=100,
prepend='<span class="input-group-addon"><span id="threshold">'+form.spam_threshold.data.__str__()+'</span>&nbsp;/&nbsp;100</span>',
prepend='<span class="input-group-text"><span id="threshold">'+form.spam_threshold.data.__str__()+'</span>&nbsp;/&nbsp;100</span>',
oninput='$("#threshold").text(this.value);') }}
{% endcall %}
{% call macros.box(title=_("Auto-forward")) %}
{% call macros.card(title=_("Auto-forward")) %}
{{ macros.form_field(form.forward_enabled,
onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')}
else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }}

@ -11,8 +11,8 @@
{% block content %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{% call macros.box() %}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
{% call macros.card() %}
{{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }}
{% if form.captcha %}
{{ macros.form_field(form.captcha) }}

@ -1,4 +1,4 @@
__all__ = [
'admins', 'aliases', 'alternatives', 'base', 'domains', 'fetches',
'managers', 'users', 'relays', 'tokens'
'managers', 'users', 'relays', 'tokens', 'languages'
]

@ -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))

@ -0,0 +1,9 @@
from mailu.ui import ui, forms, access
import flask
@ui.route('/language/<language>', methods=['GET'])
def set_language(language=None):
flask.session['language'] = language
return flask.redirect(flask.url_for('.user_settings'))

@ -46,8 +46,16 @@ babel = flask_babel.Babel()
@babel.localeselector
def get_locale():
""" selects locale for translation """
translations = [str(translation) for translation in babel.list_translations()]
return flask.request.accept_languages.best_match(translations)
translations = list(map(str, babel.list_translations()))
flask.session['available_languages'] = translations
try:
language = flask.session['language']
except KeyError:
language = flask.request.accept_languages.best_match(translations)
flask.session['language'] = language
return language
# Proxy fixer

@ -2,34 +2,30 @@
"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",
"admin-lte": "^2.4.10",
"babel-loader": "^8.0.5",
"bootstrap": "^3.4.1",
"css-loader": "^2.1.1",
"expose-loader": "^0.7.5",
"file-loader": "^3.0.1",
"font-awesome": "^4.7.0",
"font-awesome-loader": "^1.0.2",
"jQuery": "^1.7.4",
"less": "^3.9.0",
"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",
"select2": "^4.0.7-rc.0",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.2"
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"admin-lte": "^3.1.0",
"babel-loader": "^8.2.2",
"css-loader": "^6.2.0",
"expose-loader": "^3.0.0",
"jquery": "^3.6.0",
"less": "^4.1.1",
"less-loader": "^10.0.1",
"mini-css-extract-plugin": "^2.2.0",
"node-sass": "^6.0.1",
"sass-loader": "^12.1.0",
"select2": "^4.0.13",
"webpack": "^5.50.0",
"webpack-cli": "^4.7.2"
}
}

@ -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
@ -25,7 +25,7 @@ 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
@ -36,11 +36,11 @@ passlib==1.7.4
psycopg2==2.8.2
pycparser==2.19
Pygments==2.8.1
pyOpenSSL==19.0.0
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

@ -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 ",

@ -30,19 +30,23 @@ module.exports = {
test: /\.css$/,
use: [css.loader, 'css-loader']
},
{
test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/,
use: ['url-loader']
},
{
// Exposes jQuery for use outside Webpack build
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: 'jQuery'
}, {
loader: 'expose-loader',
options: '$'
options: {
exposes: [
{
globalName: '$',
override: true,
},
{
globalName: 'jQuery',
override: true,
},
]
},
}]
}
]

@ -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

@ -1,5 +1,12 @@
#!/bin/bash
tee >(rspamc -h {{ ANTISPAM_WEBUI_ADDRESS }} -P mailu learn_ham /dev/stdin) \
>(rspamc -h {{ ANTISPAM_WEBUI_ADDRESS }} -P mailu -f 11 fuzzy_del /dev/stdin) \
| rspamc -h {{ ANTISPAM_WEBUI_ADDRESS }} -P mailu -f 13 fuzzy_add /dev/stdin
RSPAMD_HOST="$(getent hosts {{ ANTISPAM_WEBUI_ADDRESS }}|cut -d\ -f1)"
if [[ $? -ne 0 ]]
then
echo "Failed to lookup {{ ANTISPAM_WEBUI_ADDRESS }}" >&2
exit 1
fi
tee >(rspamc -h $RSPAMD_HOST -P mailu learn_ham /dev/stdin) \
>(rspamc -h $RSPAMD_HOST -P mailu -f 11 fuzzy_del /dev/stdin) \
| rspamc -h $RSPAMD_HOST -P mailu -f 13 fuzzy_add /dev/stdin

@ -1,5 +1,13 @@
#!/bin/bash
tee >(rspamc -h {{ ANTISPAM_WEBUI_ADDRESS }} -P mailu learn_spam /dev/stdin) \
>(rspamc -h {{ ANTISPAM_WEBUI_ADDRESS }} -P mailu -f 13 fuzzy_del /dev/stdin) \
| rspamc -h {{ ANTISPAM_WEBUI_ADDRESS }} -P mailu -f 11 fuzzy_add /dev/stdin
RSPAMD_HOST="$(getent hosts {{ ANTISPAM_WEBUI_ADDRESS }}|cut -d\ -f1)"
if [[ $? -ne 0 ]]
then
echo "Failed to lookup {{ ANTISPAM_WEBUI_ADDRESS }}" >&2
exit 1
fi
tee >(rspamc -h $RSPAMD_HOST -P mailu learn_spam /dev/stdin) \
>(rspamc -h $RSPAMD_HOST -P mailu -f 13 fuzzy_del /dev/stdin) \
| rspamc -h $RSPAMD_HOST -P mailu -f 11 fuzzy_add /dev/stdin

@ -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 \

@ -1,13 +1,11 @@
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e
8W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx
iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K
zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI=
MIIBiAKCAYEAtQlUSOKGjpdXJ154qmMEa1pEs+9CdSxWiZFkiXBJb0lTafOh8cfF
2IkcWSwzxWwjW4Ad26UQQFh1poGf2QBzVk2vuKCekYzPAs/WqH8VwiXBiWR5R9lh
v/+CkEBYuQOzAhXLN6ZGdPPa2sjdI49rlaIqyLJE4D0TI/VHYmC/vEwqkJUgaGrS
19LhHZimnmouvrnyBPyf00czXlMow0RnmYeHVZ7W5hu7t9TH9o3QAN/GKiFfxFj+
RkdLM7beQdS0He5YeTaElM5l1YT5d5gHFbOzEQyKHd10ux+bgVcgUeVbBnI1SAIC
w53yc1PkDAiRijSP5j5aWq1djtJPheS13o35HyIf0cHzkNYhKfX5JWPj/cbgdM+C
FL1bnRc8sL5oxmkDoGJhiNZIf4n2WtS8Zu28gUgat6S+vCm/4yavIc/T1g6UiNKE
X41HPbsma/QWUwOL6S+b2qr+7rKqjI5TzVek8vBMellEV4mBvfQU3NDSQ4WvxbTq
ZEOgLPA178nrAgEC
-----END DH PARAMETERS-----

@ -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 }};
@ -250,6 +250,7 @@ mail {
listen 10025;
protocol smtp;
smtp_auth plain;
auth_http_header Auth-Port 10025;
}
# Default IMAP server for the webmail (no encryption, but authentication)
@ -257,6 +258,7 @@ mail {
listen 10143;
protocol imap;
smtp_auth plain;
auth_http_header Auth-Port 10043;
}
# SMTP is always enabled, to avoid losing emails when TLS is failing
@ -271,6 +273,7 @@ mail {
{% endif %}
protocol smtp;
smtp_auth none;
auth_http_header Auth-Port 25;
}
# All other protocols are disabled if TLS is failing
@ -283,6 +286,7 @@ mail {
{% endif %}
protocol imap;
imap_auth plain;
auth_http_header Auth-Port 143;
}
server {
@ -293,6 +297,7 @@ mail {
{% endif %}
protocol pop3;
pop3_auth plain;
auth_http_header Auth-Port 110;
}
server {
@ -303,6 +308,7 @@ mail {
{% endif %}
protocol smtp;
smtp_auth plain login;
auth_http_header Auth-Port 587;
}
{% if TLS %}
@ -311,6 +317,7 @@ mail {
listen [::]:465 ssl;
protocol smtp;
smtp_auth plain login;
auth_http_header Auth-Port 465;
}
server {
@ -318,6 +325,7 @@ mail {
listen [::]:993 ssl;
protocol imap;
imap_auth plain;
auth_http_header Auth-Port 993;
}
server {
@ -325,6 +333,7 @@ mail {
listen [::]:995 ssl;
protocol pop3;
pop3_auth plain;
auth_http_header Auth-Port 995;
}
{% endif %}
{% endif %}

@ -1,6 +1,12 @@
# Default proxy setup
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header True-Client-IP $remote_addr;
proxy_set_header Forwarded "";
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
{% if REAL_IP_HEADER and REAL_IP_FROM %}
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
{% else %}
proxy_set_header X-Forwarded-For $remote_addr;
{% endif %}
proxy_http_version 1.1;

@ -1,5 +1,10 @@
ssl_certificate {{ TLS[0] }};
ssl_certificate_key {{ TLS[1] }};
{% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %}
ssl_certificate {{ TLS[2] }};
ssl_certificate_key {{ TLS[3] }};
ssl_trusted_certificate /etc/ssl/certs/ca-cert-DST_Root_CA_X3.pem;
{% endif %}
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_dhparam /conf/dhparam.pem;

@ -26,11 +26,11 @@ cert_name = os.getenv("TLS_CERT_FILENAME", default="cert.pem")
keypair_name = os.getenv("TLS_KEYPAIR_FILENAME", default="key.pem")
args["TLS"] = {
"cert": ("/certs/%s" % cert_name, "/certs/%s" % keypair_name),
"letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem",
"/certs/letsencrypt/live/mailu/privkey.pem"),
"letsencrypt": ("/certs/letsencrypt/live/mailu/nginx-chain.pem",
"/certs/letsencrypt/live/mailu/privkey.pem", "/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem", "/certs/letsencrypt/live/mailu-ecdsa/privkey.pem"),
"mail": ("/certs/%s" % cert_name, "/certs/%s" % keypair_name),
"mail-letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem",
"/certs/letsencrypt/live/mailu/privkey.pem"),
"mail-letsencrypt": ("/certs/letsencrypt/live/mailu/nginx-chain.pem",
"/certs/letsencrypt/live/mailu/privkey.pem", "/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem", "/certs/letsencrypt/live/mailu-ecdsa/privkey.pem"),
"notls": None
}[args["TLS_FLAVOR"]]

@ -4,7 +4,6 @@ import os
import time
import subprocess
command = [
"certbot",
"-n", "--agree-tos", # non-interactive
@ -14,16 +13,47 @@ command = [
"--cert-name", "mailu",
"--preferred-challenges", "http", "--http-01-port", "8008",
"--keep-until-expiring",
"--rsa-key-size", "4096",
"--renew-with-new-domains",
"--config-dir", "/certs/letsencrypt",
"--post-hook", "/config.py"
]
command2 = [
"certbot",
"-n", "--agree-tos", # non-interactive
"-d", os.environ["HOSTNAMES"],
"-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]),
"certonly", "--standalone",
"--cert-name", "mailu-ecdsa",
"--preferred-challenges", "http", "--http-01-port", "8008",
"--keep-until-expiring",
"--key-type", "ecdsa",
"--renew-with-new-domains",
"--config-dir", "/certs/letsencrypt",
"--post-hook", "/config.py"
]
def format_for_nginx(fullchain, output):
""" We may want to strip ISRG Root X1 out
"""
certs = []
with open(fullchain, 'r') as pem:
cert = ''
for line in pem:
cert += line
if '-----END CERTIFICATE-----' in line:
certs += [cert]
cert = ''
with open(output, 'w') as pem:
for cert in certs[:-1] if len(certs)>2 and os.getenv('LETSENCRYPT_SHORTCHAIN', default="False") else certs:
pem.write(cert)
# Wait for nginx to start
time.sleep(5)
# Run certbot every hour
# Run certbot every day
while True:
subprocess.call(command)
time.sleep(3600)
format_for_nginx('/certs/letsencrypt/live/mailu/fullchain.pem', '/certs/letsencrypt/live/mailu/nginx-chain.pem')
subprocess.call(command2)
format_for_nginx('/certs/letsencrypt/live/mailu-ecdsa/fullchain.pem', '/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem')
time.sleep(86400)

@ -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

@ -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

@ -32,8 +32,9 @@ mydestination =
relayhost = {{ RELAYHOST }}
{% if RELAYUSER %}
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous, noplaintext
smtp_sasl_tls_security_options = noanonymous
{% endif %}
# Recipient delimiter for extended addresses
@ -50,15 +51,20 @@ smtpd_authorized_xclient_hosts={{ POD_ADDRESS_RANGE or SUBNET }}
# General TLS configuration
tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA
tls_preempt_cipherlist = yes
tls_ssl_options = NO_COMPRESSION
tls_ssl_options = NO_COMPRESSION, NO_TICKET
# By default, outgoing TLS is more flexible because
# 1. not all receiving servers will support TLS,
# 2. not all will have and up-to-date TLS stack.
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_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }}
smtp_tls_policy_maps=lmdb:/etc/postfix/tls_policy.map
smtp_tls_CApath = /etc/ssl/certs
smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache
smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache
smtp_host_lookup = dns
smtp_dns_support_level = dnssec
###############
# Virtual
@ -99,6 +105,8 @@ smtpd_sender_login_maps = ${podop}senderlogin
# Restrictions for incoming SMTP, other restrictions are applied in master.cf
smtpd_helo_required = yes
check_ratelimit = check_sasl_access ${podop}senderrate
smtpd_client_restrictions =
permit_mynetworks,
check_sender_access ${podop}senderaccess,

@ -7,7 +7,8 @@ smtp inet n - n - - smtpd
# Internal SMTP service
10025 inet n - n - - smtpd
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
-o smtpd_discard_ehlo_keywords=pipelining
-o smtpd_client_restrictions=$check_ratelimit,reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
-o smtpd_reject_unlisted_recipient={% if REJECT_UNLISTED_RECIPIENT %}{{ REJECT_UNLISTED_RECIPIENT }}{% else %}no{% endif %}
-o cleanup_service_name=outclean
outclean unix n - n - 0 cleanup

@ -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

@ -1 +1,2 @@
{{ RELAYHOST }} {{ RELAYUSER }}:{{ RELAYPASSWORD }}
{{ RELAYHOST }} {{ RELAYUSER }}:{{ RELAYPASSWORD }}

@ -8,12 +8,14 @@ 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)
os.mkdir('/dev/shm/postfix',mode=0o700)
url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/"
# TODO: Remove verbosity setting from Podop?
run_server(0, "postfix", "/tmp/podop.socket", [
@ -24,7 +26,8 @@ def start_podop():
("recipientmap", "url", url + "recipient/map/§"),
("sendermap", "url", url + "sender/map/§"),
("senderaccess", "url", url + "sender/access/§"),
("senderlogin", "url", url + "sender/login/§")
("senderlogin", "url", url + "sender/login/§"),
("senderrate", "url", url + "sender/rate/§")
])
def is_valid_postconf_line(line):
@ -36,6 +39,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)))
@ -56,6 +68,12 @@ for map_file in glob.glob("/overrides/*.map"):
os.system("postmap {}".format(destination))
os.remove(destination)
if not os.path.exists("/etc/postfix/tls_policy.map.db"):
with open("/etc/postfix/tls_policy.map", "w") as f:
for domain in ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'comcast.net', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'mail.ru', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'googlemail.com', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'wp.pl', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']:
f.write(f'{domain}\tsecure\n')
os.system("postmap /etc/postfix/tls_policy.map")
if "RELAYUSER" in os.environ:
path = "/etc/postfix/sasl_passwd"
conf.jinja("/conf/sasl_passwd", os.environ, path)

@ -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 \

@ -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

@ -28,6 +28,6 @@ entryPoint = "http"
# This should include all of your mail domains, and main= should be your $TRAEFIK_DOMAIN
[[acme.domains]]
main = "mail.your.doma.in"
sans = ["web.mail.your.doma.in", "smtp.mail.doma.in", "imap.mail.doma.in"]
main = "mail.example.com"
sans = ["web.mail.example.com", "smtp.mail.example.com", "imap.mail.example.com"]

@ -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/'),

@ -37,11 +37,13 @@ The ``POSTMASTER`` is the local part of the postmaster email address. It is
recommended to setup a generic value and later configure a mail alias for that
address.
The ``WILDCARD_SENDERS`` setting is a comma delimited list of user email addresses that are allowed to send emails from any existing address (spoofing the sender).
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.
@ -70,8 +72,8 @@ mail in following format: ``[HOST]:PORT``.
``RELAYUSER`` and ``RELAYPASSWORD`` can be used when authentication is needed.
By default postfix uses "opportunistic TLS" for outbound mail. This can be changed
by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt``. This setting is highly recommended
if you are a relayhost that supports TLS.
by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is highly recommended
if you are using a relayhost that supports TLS.
Similarily by default nginx uses "opportunistic TLS" for inbound mail. This can be changed
by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidden for
@ -99,14 +101,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.
@ -158,6 +165,12 @@ See the `python docs`_ for more information.
.. _`python docs`: https://docs.python.org/3.6/library/logging.html#logging-levels
The ``LETSENCRYPT_SHORTCHAIN`` (default: False) setting controls whether we send the ISRG Root X1 certificate in TLS handshakes. This is required for `android handsets older than 7.1.1` but slows down the performance of modern devices.
.. _`android handsets older than 7.1.1`: https://community.letsencrypt.org/t/production-chain-changes/150739
The ``REAL_IP_HEADER`` (default: unset) and ``REAL_IP_FROM`` (default: unset) settings controls whether HTTP headers such as ``X-Forwarded-For`` or ``X-Real-IP`` should be trusted. The former should be the name of the HTTP header to extract the client IP address from and the later a comma separated list of IP addresses designing which proxies to trust. If you are using Mailu behind a reverse proxy, you should set both. Setting the former without the later introduces a security vulnerability allowing a potential attacker to spoof his source address.
Antivirus settings
------------------

@ -267,6 +267,8 @@ correct syntax. The following file names will be taken as override configuration
- `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 <web_settings>`).
*Issue reference:* `206`_, `1368`_.
I want to integrate Nextcloud 15 (and newer) with Mailu
@ -497,6 +499,8 @@ follow these steps:
logging:
driver: journald
options:
tag: mailu-front
2. Add the /etc/fail2ban/filter.d/bad-auth.conf
@ -506,6 +510,7 @@ follow these steps:
[Definition]
failregex = .* client login failed: .+ client:\ <HOST>
ignoreregex =
journalmatch = CONTAINER_TAG=mailu-front
3. Add the /etc/fail2ban/jail.d/bad-auth.conf
@ -513,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

@ -1,63 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-admin
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-admin
role: mail
tier: backend
spec:
containers:
- name: admin
image: mailu/admin:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- name: maildata
mountPath: /data
subPath: maildata
- name: maildata
mountPath: /dkim
subPath: dkim
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
memory: 500Mi
cpu: 500m
limits:
memory: 500Mi
cpu: 500m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: admin
namespace: mailu-mailserver
labels:
app: mailu-admin
role: mail
tier: backend
spec:
selector:
app: mailu-admin
role: mail
tier: backend
ports:
- name: http
port: 80
protocol: TCP

@ -1,175 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mailu-config
namespace: mailu-mailserver
data:
# Mailu main configuration file
#
# Most configuration variables can be modified through the Web interface,
# these few settings must however be configured before starting the mail
# server and require a restart upon change.
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
ROOT: "/mailu"
# Mailu version to run (1.0, 1.1, etc. or master)
VERSION: "master"
# Set to a randomly generated 16 bytes string
SECRET_KEY: "MySup3rS3cr3tPas"
# Address where listening ports should bind
BIND_ADDRESS4: "127.0.0.1"
#BIND_ADDRESS6: "::1"
# Main mail domain
DOMAIN: "example.com"
# Hostnames for this server, separated with comas
HOSTNAMES: "mail.example.com"
# Postmaster local part (will append the main mail domain)
POSTMASTER: "admin"
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
TLS_FLAVOR: "cert"
# Authentication rate limit (per source IP address)
AUTH_RATELIMIT: "10/minute;1000/hour"
# Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS: "False"
###################################
# Kubernetes configuration
###################################
# Use Kubernetes Ingress Controller to handle all actions on port 80 and 443
# This way we can make use of the advantages of the cert-manager deployment
KUBERNETES_INGRESS: "true"
# POD_ADDRESS_RANGE is normally provided by default with Kubernetes
# Only use this value when you are using Flannel, Calico or a special kind of CNI
# Provide the IPs of your network interface or bridge which is used for VXLAN network traffic
# POD_ADDRESS_RANGE: 10.2.0.0/16,10.1.6.0/24
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN: "true"
# Run the admin interface in debug mode
#DEBUG: "True"
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL: "roundcube"
# Dav server implementation (value: radicale, none)
WEBDAV: "radicale"
# Antivirus solution (value: clamav, none)
ANTIVIRUS: "clamav"
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT: "50000000"
# Will relay all outgoing mails if configured
#RELAYHOST=
# This part is needed for the XCLIENT login for postfix. This should be the POD ADDRESS range
FRONT_ADDRESS: "front.mailu-mailserver.svc.cluster.local"
# This value is needed by the webmail to find the correct imap backend
IMAP_ADDRESS: "imap.mailu-mailserver.svc.cluster.local"
# This value is used by Dovecot to find the Redis server in the cluster
REDIS_ADDRESS: "redis.mailu-mailserver.svc.cluster.local"
# Fetchmail delay
FETCHMAIL_DELAY: "600"
# Recipient delimiter, character used to delimiter localpart from custom address part
# e.g. localpart+custom@domain;tld
RECIPIENT_DELIMITER: "+"
# DMARC rua and ruf email
DMARC_RUA: "root"
DMARC_RUF: "root"
# Welcome email, enable and set a topic and body if you wish to send welcome
# emails to all users.
WELCOME: "false"
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!"
###################################
# Web settings
###################################
# Path to the admin interface if enabled
# Kubernetes addition: You need to change ALL the ingresses, when you want this URL to be different!!!
WEB_ADMIN: "/admin"
# Path to the webmail if enabled
# Currently, this is not used, because we intended to use a different subdomain: webmail.example.com
# This option can be added in a feature release
WEB_WEBMAIL: "/webmail"
# Website name
SITENAME: "Mailu"
# Linked Website URL
WEBSITE: "https://example.com"
# Registration reCaptcha settings (warning, this has some privacy impact)
# RECAPTCHA_PUBLIC_KEY=
# RECAPTCHA_PRIVATE_KEY=
# Domain registration, uncomment to enable
# DOMAIN_REGISTRATION=true
###################################
# Advanced settings
###################################
# Create an admin account if it does not exist yet. It will also create the email domain for the account.
# INITIAL_ADMIN_ACCOUNT: "admin"
# INITIAL_ADMIN_DOMAIN: "example.com"
# INITIAL_ADMIN_PW: "s3cr3t"
# Docker-compose project name, this will prepended to containers names.
COMPOSE_PROJECT_NAME: "mailu"
# Default password scheme used for newly created accounts and changed passwords
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME: "SHA512-CRYPT"
# Header to take the real ip from
#REAL_IP_HEADER:
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
#REAL_IP_FROM:
# Host settings
HOST_IMAP: "imap.mailu-mailserver.svc.cluster.local"
HOST_POP3: "imap.mailu-mailserver.svc.cluster.local"
HOST_SMTP: "smtp.mailu-mailserver.svc.cluster.local"
HOST_AUTHSMTP: "smtp.mailu-mailserver.svc.cluster.local"
HOST_WEBMAIL: "webmail.mailu-mailserver.svc.cluster.local"
HOST_ADMIN: "admin.mailu-mailserver.svc.cluster.local"
HOST_WEBDAV: "webdav.mailu-mailserver.svc.cluster.local:5232"
HOST_ANTISPAM_MILTER: "antispam.mailu-mailserver.svc.cluster.local:11332"
HOST_ANTISPAM_WEBUI: "antispam.mailu-mailserver.svc.cluster.local:11334"
HOST_ANTIVIRUS: "antivirus.mailu-mailserver.svc.cluster.local:3310"
HOST_REDIS: "redis.mailu-mailserver.svc.cluster.local"

@ -1,39 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-fetchmail
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-fetchmail
role: mail
tier: backend
spec:
containers:
- name: fetchmail
image: mailu/fetchmail:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- name: maildata
mountPath: /data
subPath: maildata
ports:
- containerPort: 5232
- containerPort: 80
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 100Mi
cpu: 100m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage

@ -1,148 +0,0 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: mailu-front
namespace: mailu-mailserver
labels:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
spec:
selector:
matchLabels:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
template:
metadata:
labels:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/node
operator: Exists
nodeSelector:
node-role.kubernetes.io/node: ""
dnsPolicy: ClusterFirstWithHostNet
restartPolicy: Always
terminationGracePeriodSeconds: 60
containers:
- name: front
image: mailu/nginx:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- name: certs
mountPath: /certs
ports:
- name: pop3
containerPort: 110
hostPort: 110
protocol: TCP
- name: pop3s
containerPort: 995
hostPort: 995
protocol: TCP
- name: imap
containerPort: 143
hostPort: 143
protocol: TCP
- name: imaps
containerPort: 993
hostPort: 993
protocol: TCP
- name: smtp
containerPort: 25
hostPort: 25
protocol: TCP
- name: smtps
containerPort: 465
hostPort: 465
protocol: TCP
- name: smtpd
containerPort: 587
hostPort: 587
protocol: TCP
# internal services (not exposed externally)
- name: smtp-auth
containerPort: 10025
protocol: TCP
- name: imap-auth
containerPort: 10143
protocol: TCP
- name: auth
containerPort: 8000
protocol: TCP
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
volumes:
- name: certs
secret:
items:
- key: tls.crt
path: cert.pem
- key: tls.key
path: key.pem
secretName: letsencrypt-certs-all
---
apiVersion: v1
kind: Service
metadata:
name: front
namespace: mailu-mailserver
labels:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
spec:
selector:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
ports:
- name: pop3
port: 110
protocol: TCP
- name: pop3s
port: 995
protocol: TCP
- name: imap
port: 143
protocol: TCP
- name: imaps
port: 993
protocol: TCP
- name: smtp
port: 25
protocol: TCP
- name: smtps
port: 465
protocol: TCP
- name: smtpd
port: 587
protocol: TCP
- name: smtp-auth
port: 10025
protocol: TCP
- name: imap-auth
port: 10143
protocol: TCP
- name: http
port: 80
protocol: TCP

@ -1,84 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-imap
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-imap
role: mail
tier: backend
spec:
containers:
- name: imap
image: mailu/dovecot:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- mountPath: /data
name: maildata
subPath: maildata
- mountPath: /mail
name: maildata
subPath: mailstate
- mountPath: /overrides
name: maildata
subPath: overrides
ports:
- name: imap-auth
containerPort: 2102
- name: imap-transport
containerPort: 2525
- name: pop3
containerPort: 110
- name: imap-default
containerPort: 143
- name: sieve
containerPort: 4190
resources:
requests:
memory: 1Gi
cpu: 1000m
limits:
memory: 1Gi
cpu: 1000m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: imap
namespace: mailu-mailserver
labels:
app: mailu
role: mail
tier: backend
spec:
selector:
app: mailu-imap
role: mail
tier: backend
ports:
- name: imap-auth
port: 2102
protocol: TCP
- name: imap-transport
port: 2525
protocol: TCP
- name: pop3
port: 110
protocol: TCP
- name: imap-default
port: 143
protocol: TCP
- name: sieve
port: 4190
protocol: TCP

@ -3,222 +3,9 @@
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).
Please see `the Helm Chart documentation`_.
Prequisites
-----------
We are looking for maintainers: if you are interested please join our `Matrix`_ room.
Structure
~~~~~~~~~
Theres chosen to have a double NGINX stack for Mailu, this way the main
ingress can still be used to access other websites/domains on your
cluster. This is the current structure:
- ``NGINX Ingress controller``: Listens to the nodes ports 80 & 443. We have chosen to have a double NGINX stack for Mailu.
- ``Cert manager``: Creates automatic Lets Encrypt certificates based on an ``Ingress``-objects domain name.
- ``Mailu NGINX Front daemonset``: This daemonset runs in parallel with the Nginx Ingress Controller and only listens on all E-mail specific ports (25, 110, 143, 587,...). It also listens on 80 and delegates the various http endpoints to the correct services.
- ``Mailu components``: All Mailu components (imap, smtp, security, webmail,...) are split into separate files to make them more handy to use, you can find the ``YAML`` files in this directory
What you need
~~~~~~~~~~~~~
- A working Kubernetes cluster (tested with 1.10.5)
- A working `cert-manager`_ installation
- A working nginx-ingress controller needed for the lets-encrypt
certificates. You can find those files in the ``nginx`` subfolder.
Other ingress controllers that support cert-manager (e.g. traefik)
should also work.
Cert manager
^^^^^^^^^^^^
The ``Cert-manager`` is quite easy to deploy using Helm when reading the
`docs`_. After booting the ``Cert-manager`` youll need a
``ClusterIssuer`` which takes care of all required certificates through
``Ingress`` items. We chose to provide a ``clusterIssuer`` so you can provide SSL certificates
for other namespaces (different websites/services), if you don't need this option, you can easily change this by
changing ``clusterIssuer`` to ``Issuer`` and adding the ``namespace: mailu-mailserver`` to the metadata.
An example of a production and a staging ``clusterIssuer``:
.. code:: yaml
# This clusterIssuer example uses the staging environment for testing first
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-stage
spec:
acme:
email: something@example.com
http01: {}
privateKeySecretRef:
name: letsencrypt-stage
server: https://acme-staging-v02.api.letsencrypt.org/directory
.. code:: yaml
# This clusterIssuer example uses the production environment
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: something@example.com
http01: {}
privateKeySecretRef:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
**IMPORTANT**: ``ingress.yaml`` uses the ``letsencrypt-stage`` ``clusterIssuer``. If you are ready for production,
change this field in ``ingress.yaml`` file to ``letsencrypt-prod`` or whatever name you chose for the production.
If you choose for ``Issuer`` instead of ``clusterIssuer`` you also need to change the annotation to ``certmanager.k8s.io/issuer`` instead of ``certmanager.k8s.io/cluster-issuer``
Deploying Mailu
---------------
All manifests can be found in the ``mailu`` subdirectory. All commands
below need to be run from this subdirectory
Personalization
~~~~~~~~~~~~~~~
- All services run in the same namespace, currently ``mailu-mailserver``. So if you want to use a different one, change the ``namespace`` value in **every** file
- Check the ``storage-class`` field in the ``pvc.yaml`` file, you can also change the sizes to your liking. Note that you need ``RWX`` (read-write-many) and ``RWO`` (read-write-once) storageclasses.
- Check the ``configmap.yaml`` and adapt it to your needs. Be sure to check the kubernetes DNS values at the end (if you use a different namespace)
- Check the ``ingress.yaml`` file and change it to the domain you want (this is for the kubernetes ingress controller to handle the admin, webmail, webdav and auth connections)
Installation
------------
Boot the Mailu components
~~~~~~~~~~~~~~~~~~~~~~~~~
To start Mailu, run the following commands from the ``docs/kubernetes/mailu`` directory
.. code-block:: bash
kubectl create -f rbac.yaml
kubectl create -f configmap.yaml
kubectl create -f pvc.yaml
kubectl create -f redis.yaml
kubectl create -f front.yaml
kubectl create -f webmail.yaml
kubectl create -f imap.yaml
kubectl create -f security.yaml
kubectl create -f smtp.yaml
kubectl create -f fetchmail.yaml
kubectl create -f admin.yaml
kubectl create -f webdav.yaml
kubectl create -f ingress.yaml
Create the first admin account
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When the cluster is online you need to create you master user to access https://mail.example.com/admin
You can create it now manually, or have the system create it automatically.
If you want the system to create the admin user account automatically, see :ref:`admin_account`
about the environment variables needed (``INITIAL_ADMIN_*``).
Also, important, taking into consideration that a pod in Kubernetes can be stopped/rescheduled at
any time, you should set ``INITIAL_ADMIN_MODE`` to either ``update`` or ``ifmissing`` - depending on what you
want to happen to its password.
To create the admin user account manually, enter the main ``admin`` pod:
.. code-block:: bash
kubectl -n mailu-mailserver get po
kubectl -n mailu-mailserver exec -it mailu-admin-.... /bin/sh
And in the pod run the following command. The command uses following entries:
.. code-block:: bash
flask mailu admin root example.com password
- ``admin`` Make it an admin user
- ``root`` The first part of the e-mail address (ROOT@example.com)
- ``example.com`` the domain appendix
- ``password`` the chosen password for the user
Now you should be able to login on the mail account: https://mail.example.com/admin
Adaptations
-----------
Dovecot
~~~~~~~
- If you are using Dovecot on a shared file system (Glusterfs, NFS,...), you need to create a special override otherwise a lot of indexing errors will occur on your Dovecot pod.
- I also higher the number of max connections per IP. Now it's limited to 10.
Enter the dovecot pod:
.. code:: bash
kubectl -n mailu-mailserver get po
kubectl -n mailu-mailserver exec -it mailu-imap-.... /bin/sh
Create the file ``overrides/dovecot.conf``
.. code:: bash
vi /overrides/dovecot.conf
And enter following contents:
.. code:: bash
mail_nfs_index = yes
mail_nfs_storage = yes
mail_fsync = always
mmap_disable = yes
mail_max_userip_connections=100
Save and close the file and delete the imap pod to get it recreated.
.. code:: bash
kubectl -n mailu-mailserver delete po/mailu-imap-....
Wait for the pod to recreate and you're online!
Happy mailing!
.. _here: https://github.com/hacor/Mailu/blob/master/core/postfix/conf/main.cf#L35
.. _cert-manager: https://github.com/jetstack/cert-manager
.. _docs: https://cert-manager.io/docs/installation/kubernetes/#installing-with-helm
Imap login fix
~~~~~~~~~~~~~~
If it seems you're not able to login using IMAP on your Mailu accounts, check the logs of the imap container to see whether it's a permissions problem on the database.
This problem can be easily fixed by running following commands:
.. code:: bash
kubectl -n mailu-mailserver exec -it mailu-imap-... /bin/sh
chmod 777 /data/main.db
If the login problem still persists, or more specific, happens now and then and you see some Auth problems on your webmail or mail client, try following steps:
- Add ``auth_debug=yes`` to the ``/overrides/dovecot.conf`` file and delete the pod in order to start a new one, which loads the configuration
- Depending on your network configuration you could still see some ``allow_nets check failed`` results in the logs. This means that the IP is not allowed a login
- If this is happening your network plugin has troubles with the Nginx Ingress Controller using the ``hostNetwork: true`` option. Known cases: Flannel and Calico.
- You should uncomment ``POD_ADDRESS_RANGE`` in the ``configmap.yaml`` file and add the IP range of your pod network bridge (the range that sadly has failed the ``allowed_nets`` test)
- Delete the Admin pod and wait for it to restart
.. code:: bash
kubectl -n mailu-mailserver get po
kubectl -n mailu-mailserver delete po/mailu-admin...
Happy mailing!
.. _`the Helm Chart documentation`: https://github.com/Mailu/helm-charts/blob/master/mailu/README.md
.. _`Matrix`: https://matrix.to/#/#mailu:tedomum.net

@ -1,25 +0,0 @@
apiVersion: apps/v1
kind: Ingress
metadata:
name: mailu-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
labels:
app: mailu
role: mail
tier: backend
spec:
tls:
- hosts:
- "mail.example.com"
secretName: letsencrypt-certs-all # If unsure how to generate these, check out https://github.com/ployst/docker-letsencrypt
rules:
- host: "mail.example.com"
http:
paths:
- path: "/"
backend:
serviceName: front
servicePort: 80

@ -1,27 +0,0 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: redis-hdd
namespace: mailu-mailserver
annotations:
volume.beta.kubernetes.io/storage-class: "glusterblock-hdd"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mail-storage
namespace: mailu-mailserver
annotations:
volume.beta.kubernetes.io/storage-class: "gluster-heketi-hdd"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi

@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: mailu-mailserver

@ -1,60 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-redis
namespace: mailu-mailserver
spec:
replicas: 1
selector:
matchLabels:
app: mailu-redis
role: mail
tier: backend
template:
metadata:
labels:
app: mailu-redis
role: mail
tier: backend
spec:
containers:
- name: redis
image: redis:5-alpine
imagePullPolicy: Always
volumeMounts:
- mountPath: /data
name: redisdata
ports:
- containerPort: 6379
name: redis
protocol: TCP
resources:
requests:
memory: 200Mi
cpu: 100m
limits:
memory: 300Mi
cpu: 200m
volumes:
- name: redisdata
persistentVolumeClaim:
claimName: redis-hdd
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: mailu-mailserver
labels:
app: mailu-redis
role: mail
tier: backend
spec:
selector:
app: mailu-redis
role: mail
tier: backend
ports:
- name: redis
port: 6379
protocol: TCP

@ -1,115 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-security
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-security
role: mail
tier: backend
spec:
containers:
- name: antispam
image: mailu/rspamd:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
ports:
- name: antispam
containerPort: 11332
protocol: TCP
- name: antispam-http
containerPort: 11334
protocol: TCP
volumeMounts:
- name: filter
subPath: filter
mountPath: /var/lib/rspamd
- name: filter
mountPath: /dkim
subPath: dkim
- name: filter
mountPath: /etc/rspamd/override.d
subPath: rspamd-overrides
- name: antivirus
image: mailu/clamav:master
imagePullPolicy: Always
resources:
requests:
memory: 1Gi
cpu: 1000m
limits:
memory: 2Gi
cpu: 1000m
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: antivirus
containerPort: 3310
protocol: TCP
volumeMounts:
- name: filter
subPath: filter
mountPath: /data
volumes:
- name: filter
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: antispam
namespace: mailu-mailserver
labels:
app: mailu-antispam
role: mail
tier: backend
spec:
selector:
app: mailu-security
role: mail
tier: backend
ports:
- name: antispam
port: 11332
protocol: TCP
- name: antispam-http
protocol: TCP
port: 11334
---
apiVersion: v1
kind: Service
metadata:
name: antivirus
namespace: mailu-mailserver
labels:
app: mailu-antivirus
role: mail
tier: backend
spec:
selector:
app: mailu-security
role: mail
tier: backend
ports:
- name: antivirus
port: 3310
protocol: TCP

@ -1,80 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-smtp
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-smtp
role: mail
tier: backend
spec:
containers:
- name: smtp
image: mailu/postfix:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
resources:
requests:
memory: 2Gi
cpu: 500m
limits:
memory: 2Gi
cpu: 500m
volumeMounts:
- mountPath: /queue
name: maildata
subPath: mailqueue
- mountPath: /overrides
name: maildata
subPath: overrides
ports:
- name: smtp
containerPort: 25
protocol: TCP
- name: smtp-ssl
containerPort: 465
protocol: TCP
- name: smtp-starttls
containerPort: 587
protocol: TCP
- name: smtp-auth
containerPort: 10025
protocol: TCP
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: smtp
namespace: mailu-mailserver
labels:
app: mailu
role: mail
tier: backend
spec:
selector:
app: mailu-smtp
role: mail
tier: backend
ports:
- name: smtp
port: 25
protocol: TCP
- name: smtp-ssl
port: 465
protocol: TCP
- name: smtp-starttls
port: 587
protocol: TCP
- name: smtp-auth
port: 10025
protocol: TCP

@ -1,63 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-webdav
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-webdav
role: mail
tier: backend
spec:
containers:
- name: radicale
image: mailu/radicale:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- mountPath: /data
name: maildata
subPath: dav
ports:
- containerPort: 5232
- containerPort: 80
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 100Mi
cpu: 100m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: webdav
namespace: mailu-mailserver
labels:
app: mailu-webdav
role: mail
tier: backend
spec:
selector:
app: mailu-webdav
role: mail
tier: backend
ports:
ports:
- name: http
port: 80
protocol: TCP
- name: http-ui
port: 5232
protocol: TCP

@ -1,57 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailu-roundcube
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-roundcube
role: mail
tier: frontend
spec:
containers:
- name: roundcube
image: mailu/roundcube:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
volumeMounts:
- mountPath: /data
name: maildata
subPath: webmail
ports:
- containerPort: 80
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: webmail
namespace: mailu-mailserver
labels:
app: mailu-roundcube
role: mail
tier: frontend
spec:
selector:
app: mailu-roundcube
role: mail
tier: frontend
ports:
- name: http
port: 80
protocol: TCP

@ -1,55 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: default-http-backend
labels:
app: default-http-backend
namespace: kube-ingress
spec:
replicas: 1
selector:
matchLabels:
app: default-http-backend
template:
metadata:
labels:
app: default-http-backend
spec:
terminationGracePeriodSeconds: 60
containers:
- name: default-http-backend
# Any image is permissible as long as:
# 1. It serves a 404 page at /
# 2. It serves 200 on a /healthz endpoint
image: gcr.io/google_containers/defaultbackend:1.4
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
ports:
- containerPort: 8080
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
name: default-http-backend
namespace: kube-ingress
labels:
app: default-http-backend
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: default-http-backend

@ -1,127 +0,0 @@
apiVersion: v1
kind: Service
metadata:
# keep it under 24 chars
name: ingress-lb
namespace: kube-ingress
labels:
k8s-app: ingress-lb
component: ingress-controller
spec:
type: ClusterIP
selector:
k8s-app: ingress-lb
component: ingress-controller
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
- name: https
protocol: TCP
port: 443
targetPort: 443
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: kube-ingress
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: kube-ingress
data:
---
apiVersion: v1
data:
enable-vts-status: "true"
kind: ConfigMap
metadata:
name: nginx-ingress-lb-conf
namespace: kube-ingress
---
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: ingress-controller
namespace: kube-ingress
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
labels:
k8s-app: ingress-lb
component: ingress-controller
type: nginx
spec:
updateStrategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
selector:
matchLabels:
k8s-app: ingress-lb
component: ingress-controller
type: nginx
template:
metadata:
labels:
k8s-app: ingress-lb
component: ingress-controller
type: nginx
spec:
serviceAccount: kube-nginx-ingress
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/master
operator: DoesNotExist
containers:
- name: nginx-ingress-lb
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.16.2
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --annotations-prefix=ingress.kubernetes.io
- --enable-ssl-passthrough
# use downward API
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
initialDelaySeconds: 10
timeoutSeconds: 1
httpGet:
path: /healthz
port: 10254
scheme: HTTP
hostNetwork: true
nodeSelector:
node-role.kubernetes.io/node: ""
dnsPolicy: ClusterFirstWithHostNet
restartPolicy: Always
terminationGracePeriodSeconds: 60

@ -1,129 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: kube-ingress
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-nginx-ingress
namespace: kube-ingress
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: kube-nginx-ingress
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- update
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: kube-nginx-ingress
namespace: kube-ingress
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
- create
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: kube-nginx-ingress
namespace: kube-ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kube-nginx-ingress
subjects:
- kind: ServiceAccount
name: kube-nginx-ingress
namespace: kube-ingress
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: kube-nginx-ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-nginx-ingress
subjects:
- kind: ServiceAccount
name: kube-nginx-ingress
namespace: kube-ingress

@ -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
````````````````````````````````
Heres 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.

@ -2,3 +2,4 @@ recommonmark
Sphinx
sphinx-autobuild
sphinx-rtd-theme
docutils==0.16

@ -3,14 +3,13 @@ Using an external reverse proxy
One of Mailu use cases is as part of a larger services platform, where maybe other Web services are available than Mailu Webmail and admin interface.
In such a configuration, one would usually run a frontend reverse proxy to serve all Web contents based on criteria like the requested hostname (virtual hosts) and/or the requested path. Mailu Web frontend is disabled in the default setup for security reasons, it is however expected that most users will enable it at some point. Also, due to Docker Compose configuration structure, it is impossible for us to make disabling the Web frontend completely available through a configuration variable. This guide was written to help users setup such an architecture.
In such a configuration, one would usually run a frontend reverse proxy to serve all Web contents based on criteria like the requested hostname (virtual hosts) and/or the requested path. Mailu Admin Web frontend is disabled in the default setup for security reasons, it is however expected that most users will enable it at some point. Also, due to Docker Compose configuration structure, it is impossible for us to make disabling the Web frontend completely available through a configuration variable. This guide was written to help users setup such an architecture.
There are basically three options, from the most to the least recommended one:
- have Mailu Web frontend listen locally and use your own Web frontend on top of it
- use ``Traefik`` in another container as central system-reverse-proxy
- override Mailu Web frontend configuration
- disable Mailu Web frontend completely and use your own
- `have Mailu Web frontend listen locally and use your own Web frontend on top of it`_
- `use Traefik in another container as central system-reverse-proxy`_
- `override Mailu Web frontend configuration`_
All options will require that you modify the ``docker-compose.yml`` file.
@ -89,7 +88,7 @@ Here is an example configuration :
server {
listen <public_ip>:443;
server_name yourpublicname.tld;
server_name external.example.com;
# [...] here goes your standard configuration
location /webmail {
@ -99,7 +98,7 @@ Here is an example configuration :
server {
listen <internal_ip>:443;
server_name yourinternalname.tld;
server_name internal.example.com;
# [...] here goes your standard configuration
location /admin {
@ -113,7 +112,7 @@ Depending on how you access the front server, you might want to add a ``proxy_re
.. code-block:: nginx
proxy_redirect https://localhost https://your-domain.com;
proxy_redirect https://localhost https://example.com;
This will stop redirects (301 and 302) sent by the Webmail, nginx front and admin interface from sending you to ``localhost``.
@ -151,8 +150,8 @@ Add the respective Traefik labels for your domain/configuration, like
.. note:: Please dont forget to add ``TRAEFIK_DOMAIN=[...]`` TO YOUR ``.env``
If your Traefik is configured to automatically request certificates from *letsencrypt*, then youll 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``,
If your Traefik is configured to automatically request certificates from *letsencrypt*, then youll have a certificate for ``mail.your.example.com`` now. However,
``mail.your.example.com`` might only be the location where you want the Mailu web-interfaces to live — your mail should be sent/received from ``your.example.com``,
and this is the ``DOMAIN`` in your ``.env``?
To support that use-case, Traefik can request ``SANs`` for your domain. The configuration for this will depend on your Traefik version.
@ -171,12 +170,12 @@ Add the appropriate labels for your domain(s) to the ``front`` container in ``do
# Enable TLS
- "traefik.http.routers.mailu-secure.tls"
# Your main domain
- "traefik.http.routers.mailu-secure.tls.domains[0].main=your.doma.in"
- "traefik.http.routers.mailu-secure.tls.domains[0].main=your.example.com"
# 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"
- "traefik.http.routers.mailu-secure.tls.domains[0].sans=mail.your.example.com,webmail.your.example.com,smtp.your.example.com"
# 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"
- "traefik.http.routers.mailu-secure.tls.domains[1].main=mail.other.example.com"
- "traefik.http.routers.mailu-secure.tls.domains[1].sans=mail2.other.example.com,mail3.other.example.com"
# Your ACME certificate resolver
- "traefik.http.routers.mailu-secure.tls.certResolver=foo"
@ -193,8 +192,8 @@ Lets add something like
[acme]
[[acme.domains]]
main = "your.doma.in" # this is the same as $TRAEFIK_DOMAIN!
sans = ["mail.your.doma.in", "webmail.your.doma.in", "smtp.your.doma.in"]
main = "your.example.com" # this is the same as $TRAEFIK_DOMAIN!
sans = ["mail.your.example.com", "webmail.your.example.com", "smtp.your.example.com"]
to your ``traefik.toml``.
@ -259,9 +258,7 @@ You can also download the example configuration files:
- :download:`compose/traefik/docker-compose.yml`
- :download:`compose/traefik/traefik.toml`
Disable completely Mailu reverse proxy
--------------------------------------
.. _have Mailu Web frontend listen locally and use your own Web frontend on top of it: #have-mailu-web-frontend-listen-locally
.. _use Traefik in another container as central system-reverse-proxy: #traefik-as-reverse-proxy
.. _override Mailu Web frontend configuration: #override-mailu-configuration
You must not disable Mailu reverse proxy by removing the ``front`` section from the ``docker-compose.yml``.
``front`` is handling authentication and is also proxying e.g. SMTP and IMAP. A basic HTTP reverse proxy as described in this document is not sufficient for this.

@ -315,6 +315,21 @@ This page is also accessible for domain managers. On the users page new users ca
* Fetched accounts. Access the fetched accounts page of the user. See the :ref:`fetched accounts page <webadministration_fetched_accounts>` for more information.
This page also shows an overview of the following settings of an user:
* Email. The email address of the user.
* Features. Shows if IMAP or POP3 access is enabled.
* Storage quota. Shows how much assigned storage has been consumed.
* Sending Quota. The sending quota is the limit of messages a single user can send per day.
* Comment. A desription for the user.
* Created. Date when the user was created.
* Last edit. Last date when the user was modified.
.. _webadministration_add_user:
@ -334,7 +349,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.
* Storage Quota. The maximum quota for the user's email box.
* Allow IMAP access. When ticked, allows email retrieval via the IMAP protocol.

@ -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 \

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.12
ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images

@ -1,9 +1,8 @@
ARG DISTRO=alpine:3.12
ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip bash py3-multidict \
&& apk add --upgrade sudo \
&& pip3 install --upgrade pip
# Shared layer between nginx, dovecot, postfix, postgresql, rspamd, unbound, rainloop, roundcube

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.12
ARG DISTRO=alpine:3.14
FROM $DISTRO
# python3 shared with most images

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.10
ARG DISTRO=alpine:3.14
FROM $DISTRO
RUN mkdir -p /app

@ -62,6 +62,11 @@ ANTIVIRUS={{ antivirus_enabled or 'none' }}
# Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }}
# Message rate limit (per user)
{% if message_ratelimit_pd > '0' %}
MESSAGE_RATELIMIT={{ message_ratelimit_pd }}/day
{% endif %}
# Networks granted relay permissions
# Use this with care, all hosts in this networks will be able to send mail without authentication!
RELAYNETS=

@ -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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save