Merge remote-tracking branch 'upstream/master' into extend-nginx

master
Tim Möhlmann 6 years ago
commit c00910ca4b
No known key found for this signature in database
GPG Key ID: 8677988D8072E8DE

@ -1,10 +1,25 @@
rules: pull_request_rules:
default: null - name: Successful travis and 2 approved reviews
branches: conditions:
master: - status-success=continuous-integration/travis-ci/pr
protection: - label!=["status"/wip","status/blocked"]
required_status_checks: - "#approved-reviews-by>=2"
contexts: actions:
- continuous-integration/travis-ci merge:
required_pull_request_reviews: method: merge
required_approving_review_count: 2 strict: true
dismiss_reviews:
approved: true
- name: Trusted author, successful travis and 1 approved review
conditions:
- author~=(kaiyou|muhlemmer|mildred|HorayNarea|adi90x|hoellen|ofthesun9)
- status-success=continuous-integration/travis-ci/pr
- label!=["status"/wip","status/blocked","review/need2"]
- "#approved-reviews-by>=1"
actions:
merge:
method: merge
strict: true
dismiss_reviews:
approved: true

@ -4,14 +4,30 @@ addons:
apt: apt:
packages: packages:
- docker-ce - docker-ce
env: env:
- VERSION=$TRAVIS_BRANCH - MAILU_VERSION=$TRAVIS_BRANCH
language: python
python:
- "3.6"
install:
- pip install -r tests/requirements.txt
- sudo curl -L https://github.com/docker/compose/releases/download/1.23.0-rc3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
before_script:
- docker-compose -v
- docker-compose -f tests/build.yml build
- sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
script: script:
# Default to mailu for DOCKER_ORG # test.py, test name and timeout between start and tests.
- if [ -z "$DOCKER_ORG" ]; then export DOCKER_ORG="mailu"; fi - python tests/compose/test.py core 1
- docker-compose -f tests/build.yml build - python tests/compose/test.py fetchmail 1
- tests/compose/test-script.sh - travis_wait python tests/compose/test.py filters 10
- python tests/compose/test.py rainloop 1
- python tests/compose/test.py roundcube 1
- python tests/compose/test.py webdav 1
deploy: deploy:
provider: script provider: script
@ -19,4 +35,3 @@ deploy:
on: on:
all_branches: true all_branches: true
condition: -n $DOCKER_UN condition: -n $DOCKER_UN

@ -15,14 +15,14 @@ RUN apk add --no-cache openssl curl \
COPY mailu ./mailu COPY mailu ./mailu
COPY migrations ./migrations COPY migrations ./migrations
COPY manage.py .
COPY start.py /start.py COPY start.py /start.py
RUN pybabel compile -d mailu/translations RUN pybabel compile -d mailu/translations
EXPOSE 80/tcp EXPOSE 80/tcp
VOLUME ["/data"] VOLUME ["/data"]
ENV FLASK_APP mailu
CMD /start.py CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ui || exit 1 HEALTHCHECK CMD curl -f -L http://localhost/ui/login?next=ui.index || exit 1

@ -1,140 +1,57 @@
import flask import flask
import flask_sqlalchemy
import flask_bootstrap import flask_bootstrap
import flask_login
import flask_script
import flask_migrate
import flask_babel
import flask_limiter
import os from mailu import utils, debug, models, manage, configuration
import docker
import socket
import uuid
from werkzeug.contrib import fixers, profiler
# Create application
app = flask.Flask(__name__)
default_config = {
# Specific to the admin UI
'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/main.db',
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
'DOCKER_SOCKET': 'unix:///var/run/docker.sock',
'BABEL_DEFAULT_LOCALE': 'en',
'BABEL_DEFAULT_TIMEZONE': 'UTC',
'BOOTSTRAP_SERVE_LOCAL': True,
'RATELIMIT_STORAGE_URL': 'redis://redis/2',
'QUOTA_STORAGE_URL': 'redis://redis/1',
'DEBUG': False,
'DOMAIN_REGISTRATION': False,
# Statistics management
'INSTANCE_ID_PATH': '/data/instance',
'STATS_ENDPOINT': '0.{}.stats.mailu.io',
# Common configuration variables
'SECRET_KEY': 'changeMe',
'DOMAIN': 'mailu.io',
'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io',
'POSTMASTER': 'postmaster',
'TLS_FLAVOR': 'cert',
'AUTH_RATELIMIT': '10/minute;1000/hour',
'DISABLE_STATISTICS': 'False',
# Mail settings
'DMARC_RUA': None,
'DMARC_RUF': None,
'WELCOME': 'False',
'WELCOME_SUBJECT': 'Dummy welcome topic',
'WELCOME_BODY': 'Dummy welcome body',
'DKIM_SELECTOR': 'dkim',
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
'DEFAULT_QUOTA': 1000000000,
# Web settings
'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io',
'WEB_ADMIN': '/admin',
'WEB_WEBMAIL': '/webmail',
'RECAPTCHA_PUBLIC_KEY': '',
'RECAPTCHA_PRIVATE_KEY': '',
# Advanced settings
'PASSWORD_SCHEME': 'BLF-CRYPT',
# Host settings
'HOST_IMAP': 'imap',
'HOST_POP3': 'imap',
'HOST_SMTP': 'smtp',
'HOST_WEBMAIL': 'webmail',
'HOST_FRONT': 'front',
'HOST_AUTHSMTP': os.environ.get('HOST_SMTP', 'smtp'),
'POD_ADDRESS_RANGE': None
}
# Load configuration from the environment if available
for key, value in default_config.items():
app.config[key] = os.environ.get(key, value)
# Base application
flask_bootstrap.Bootstrap(app)
db = flask_sqlalchemy.SQLAlchemy(app)
migrate = flask_migrate.Migrate(app, db)
limiter = flask_limiter.Limiter(app, key_func=lambda: current_user.username)
# Debugging toolbar
if app.config.get("DEBUG"):
import flask_debugtoolbar
toolbar = flask_debugtoolbar.DebugToolbarExtension(app)
# Profiler
if app.config.get("DEBUG"):
app.wsgi_app = profiler.ProfilerMiddleware(app.wsgi_app, restrictions=[30])
# Manager commnad
manager = flask_script.Manager(app)
manager.add_command('db', flask_migrate.MigrateCommand)
# Babel configuration
babel = flask_babel.Babel(app)
translations = list(map(str, babel.list_translations()))
@babel.localeselector
def get_locale():
return flask.request.accept_languages.best_match(translations)
# Login configuration
login_manager = flask_login.LoginManager()
login_manager.init_app(app)
login_manager.login_view = "ui.login"
@login_manager.unauthorized_handler
def handle_needs_login():
return flask.redirect(
flask.url_for('ui.login', next=flask.request.endpoint)
)
@app.context_processor
def inject_defaults():
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
return dict(
current_user=flask_login.current_user,
signup_domains=signup_domains,
config=app.config
)
# Import views
from mailu import ui, internal
app.register_blueprint(ui.ui, url_prefix='/ui')
app.register_blueprint(internal.internal, url_prefix='/internal')
# Create the prefix middleware
class PrefixMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
prefix = environ.get('HTTP_X_FORWARDED_PREFIX', '')
if prefix:
environ['SCRIPT_NAME'] = prefix
return self.app(environ, start_response)
app.wsgi_app = PrefixMiddleware(fixers.ProxyFix(app.wsgi_app)) def create_app_from_config(config):
""" Create a new application based on the given configuration
"""
app = flask.Flask(__name__)
app.app_context().push()
app.cli.add_command(manage.mailu)
# Bootstrap is used for basic JS and CSS loading
# TODO: remove this and use statically generated assets instead
app.bootstrap = flask_bootstrap.Bootstrap(app)
# Initialize application extensions
config.init_app(app)
models.db.init_app(app)
utils.limiter.init_app(app)
utils.babel.init_app(app)
utils.login.init_app(app)
utils.login.user_loader(models.User.get)
utils.proxy.init_app(app)
utils.migrate.init_app(app, models.db)
# Initialize debugging tools
if app.config.get("DEBUG"):
debug.toolbar.init_app(app)
# TODO: add a specific configuration variable for profiling
# debug.profiler.init_app(app)
# Inject the default variables in the Jinja parser
# TODO: move this to blueprints when needed
@app.context_processor
def inject_defaults():
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
return dict(
signup_domains=signup_domains,
config=app.config
)
# Import views
from mailu import ui, internal
app.register_blueprint(ui.ui, url_prefix='/ui')
app.register_blueprint(internal.internal, url_prefix='/internal')
return app
def create_app():
""" Create a new application based on the config module
"""
config = configuration.ConfigManager()
return create_app_from_config(config)

@ -0,0 +1,90 @@
import os
DEFAULT_CONFIG = {
# Specific to the admin UI
'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/main.db',
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
'DOCKER_SOCKET': 'unix:///var/run/docker.sock',
'BABEL_DEFAULT_LOCALE': 'en',
'BABEL_DEFAULT_TIMEZONE': 'UTC',
'BOOTSTRAP_SERVE_LOCAL': True,
'RATELIMIT_STORAGE_URL': 'redis://redis/2',
'QUOTA_STORAGE_URL': 'redis://redis/1',
'DEBUG': False,
'DOMAIN_REGISTRATION': False,
'TEMPLATES_AUTO_RELOAD': True,
# Statistics management
'INSTANCE_ID_PATH': '/data/instance',
'STATS_ENDPOINT': '0.{}.stats.mailu.io',
# Common configuration variables
'SECRET_KEY': 'changeMe',
'DOMAIN': 'mailu.io',
'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io',
'POSTMASTER': 'postmaster',
'TLS_FLAVOR': 'cert',
'AUTH_RATELIMIT': '10/minute;1000/hour',
'DISABLE_STATISTICS': 'False',
# Mail settings
'DMARC_RUA': None,
'DMARC_RUF': None,
'WELCOME': 'False',
'WELCOME_SUBJECT': 'Dummy welcome topic',
'WELCOME_BODY': 'Dummy welcome body',
'DKIM_SELECTOR': 'dkim',
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
'DEFAULT_QUOTA': 1000000000,
# Web settings
'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io',
'WEB_ADMIN': '/admin',
'WEB_WEBMAIL': '/webmail',
'RECAPTCHA_PUBLIC_KEY': '',
'RECAPTCHA_PRIVATE_KEY': '',
# Advanced settings
'PASSWORD_SCHEME': 'BLF-CRYPT',
# Host settings
'HOST_IMAP': 'imap',
'HOST_POP3': 'imap',
'HOST_SMTP': 'smtp',
'HOST_WEBMAIL': 'webmail',
'HOST_FRONT': 'front',
'HOST_AUTHSMTP': os.environ.get('HOST_SMTP', 'smtp'),
'POD_ADDRESS_RANGE': None
}
class ConfigManager(dict):
""" Naive configuration manager that uses environment only
"""
def __init__(self):
self.config = dict()
def init_app(self, app):
self.config.update(app.config)
self.config.update({
key: os.environ.get(key, value)
for key, value in DEFAULT_CONFIG.items()
})
app.config = self
def setdefault(self, key, value):
if key not in self.config:
self.config[key] = value
return self.config[key]
def get(self, *args):
return self.config.get(*args)
def keys(self):
return self.config.keys()
def __getitem__(self, key):
return self.config.get(key)
def __setitem__(self, key, value):
self.config[key] = value
def __contains__(self, key):
return key in self.config

@ -0,0 +1,17 @@
import flask_debugtoolbar
from werkzeug.contrib import profiler as werkzeug_profiler
# Debugging toolbar
toolbar = flask_debugtoolbar.DebugToolbarExtension()
# Profiler
class Profiler(object):
def init_app(self, app):
app.wsgi_app = werkzeug_profiler.ProfilerMiddleware(
app.wsgi_app, restrictions=[30]
)
profiler = Profiler()

@ -1,26 +0,0 @@
from mailu import app
import docker
import signal
# Connect to the Docker socket
cli = docker.Client(base_url=app.config['DOCKER_SOCKET'])
def get(*names):
result = {}
all_containers = cli.containers(all=True)
for brief in all_containers:
if brief['Image'].startswith('mailu/'):
container = cli.inspect_container(brief['Id'])
container['Image'] = cli.inspect_image(container['Image'])
name = container['Config']['Labels']['com.docker.compose.service']
if not names or name in names:
result[name] = container
return result
def reload(*names):
for name, container in get(*names).items():
cli.kill(container["Id"], signal.SIGHUP.value)

@ -1,6 +1,6 @@
from flask_limiter import RateLimitExceeded from flask_limiter import RateLimitExceeded
from mailu import limiter from mailu import utils
import socket import socket
import flask import flask
@ -19,7 +19,7 @@ def rate_limit_handler(e):
return response return response
@limiter.request_filter @utils.limiter.request_filter
def whitelist_webmail(): def whitelist_webmail():
try: try:
return flask.request.headers["Client-Ip"] ==\ return flask.request.headers["Client-Ip"] ==\

@ -1,4 +1,5 @@
from mailu import db, models, app from mailu import models
from flask import current_app as app
import re import re
import socket import socket

@ -1,5 +1,6 @@
from mailu import db, models, app, limiter from mailu import models, utils
from mailu.internal import internal, nginx from mailu.internal import internal, nginx
from flask import current_app as app
import flask import flask
import flask_login import flask_login
@ -7,7 +8,7 @@ import base64
@internal.route("/auth/email") @internal.route("/auth/email")
@limiter.limit( @utils.limiter.limit(
app.config["AUTH_RATELIMIT"], app.config["AUTH_RATELIMIT"],
lambda: flask.request.headers["Client-Ip"] lambda: flask.request.headers["Client-Ip"]
) )

@ -1,5 +1,6 @@
from mailu import db, models, app from mailu import models
from mailu.internal import internal from mailu.internal import internal
from flask import current_app as app
import flask import flask
import socket import socket
@ -36,7 +37,7 @@ def dovecot_quota(ns, user_email):
user = models.User.query.get(user_email) or flask.abort(404) user = models.User.query.get(user_email) or flask.abort(404)
if ns == "storage": if ns == "storage":
user.quota_bytes_used = flask.request.get_json() user.quota_bytes_used = flask.request.get_json()
db.session.commit() models.db.session.commit()
return flask.jsonify(None) return flask.jsonify(None)

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.internal import internal from mailu.internal import internal
import flask import flask
@ -27,6 +27,6 @@ def fetch_done(fetch_id):
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404) fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
fetch.last_check = datetime.datetime.now() fetch.last_check = datetime.datetime.now()
fetch.error_message = str(flask.request.get_json()) fetch.error_message = str(flask.request.get_json())
db.session.add(fetch) models.db.session.add(fetch)
db.session.commit() models.db.session.commit()
return "" return ""

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.internal import internal from mailu.internal import internal
import flask import flask
@ -6,7 +6,9 @@ import flask
@internal.route("/postfix/domain/<domain_name>") @internal.route("/postfix/domain/<domain_name>")
def postfix_mailbox_domain(domain_name): def postfix_mailbox_domain(domain_name):
domain = models.Domain.query.get(domain_name) or flask.abort(404) domain = models.Domain.query.get(domain_name) or \
models.Alternative.query.get(domain_name) or \
flask.abort(404)
return flask.jsonify(domain.name) return flask.jsonify(domain.name)
@ -18,37 +20,34 @@ def postfix_mailbox_map(email):
@internal.route("/postfix/alias/<alias>") @internal.route("/postfix/alias/<alias>")
def postfix_alias_map(alias): def postfix_alias_map(alias):
localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias) localpart, domain_name = models.Email.resolve_domain(alias)
alternative = models.Alternative.query.get(domain)
if alternative:
domain = alternative.domain_name
email = '{}@{}'.format(localpart, domain)
if localpart is None: if localpart is None:
return flask.jsonify(domain) return flask.jsonify(domain_name)
else: destination = models.Email.resolve_destination(localpart, domain_name)
alias_obj = models.Alias.resolve(localpart, domain) return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
if alias_obj:
return flask.jsonify(",".join(alias_obj.destination))
user_obj = models.User.query.get(email)
if user_obj:
return flask.jsonify(user_obj.destination)
return flask.abort(404)
@internal.route("/postfix/transport/<email>") @internal.route("/postfix/transport/<email>")
def postfix_transport(email): def postfix_transport(email):
localpart, domain = email.split('@', 1) if '@' in email else (None, email) if email == '*':
relay = models.Relay.query.get(domain) or flask.abort(404) return flask.abort(404)
localpart, domain_name = models.Email.resolve_domain(email)
relay = models.Relay.query.get(domain_name) or flask.abort(404)
return flask.jsonify("smtp:[{}]".format(relay.smtp)) return flask.jsonify("smtp:[{}]".format(relay.smtp))
@internal.route("/postfix/sender/<sender>") @internal.route("/postfix/sender/login/<sender>")
def postfix_sender(sender): def postfix_sender_login(sender):
localpart, domain_name = models.Email.resolve_domain(sender)
if localpart is None:
return flask.abort(404)
destination = models.Email.resolve_destination(localpart, domain_name, True)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/sender/access/<sender>")
def postfix_sender_access(sender):
""" Simply reject any sender that pretends to be from a local domain """ Simply reject any sender that pretends to be from a local domain
""" """
localpart, domain_name = sender.split('@', 1) if '@' in sender else (None, sender) localpart, domain_name = models.Email.resolve_domain(sender)
domain = models.Domain.query.get(domain_name) return flask.jsonify("REJECT") if models.Domain.query.get(domain_name) else flask.abort(404)
alternative = models.Alternative.query.get(domain_name)
if domain or alternative:
return flask.jsonify("REJECT")
return flask.abort(404)

@ -1,11 +1,26 @@
from mailu import app, manager, db, models from mailu import models
from flask import current_app as app
from flask import cli as flask_cli
import flask
import os import os
import socket import socket
import uuid import uuid
import click
@manager.command db = models.db
@click.group()
def mailu(cls=flask_cli.FlaskGroup):
""" Mailu command line
"""
@mailu.command()
@flask_cli.with_appcontext
def advertise(): def advertise():
""" Advertise this server against statistic services. """ Advertise this server against statistic services.
""" """
@ -23,7 +38,11 @@ def advertise():
pass pass
@manager.command @mailu.command()
@click.argument('localpart')
@click.argument('domain_name')
@click.argument('password')
@flask_cli.with_appcontext
def admin(localpart, domain_name, password): def admin(localpart, domain_name, password):
""" Create an admin user """ Create an admin user
""" """
@ -41,11 +60,17 @@ def admin(localpart, domain_name, password):
db.session.commit() db.session.commit()
@manager.command @mailu.command()
def user(localpart, domain_name, password, @click.argument('localpart')
hash_scheme=app.config['PASSWORD_SCHEME']): @click.argument('domain_name')
@click.argument('password')
@click.argument('hash_scheme')
@flask_cli.with_appcontext
def user(localpart, domain_name, password, hash_scheme=None):
""" Create a user """ Create a user
""" """
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
@ -60,10 +85,12 @@ def user(localpart, domain_name, password,
db.session.commit() db.session.commit()
@manager.option('-n', '--domain_name', dest='domain_name') @mailu.command()
@manager.option('-u', '--max_users', dest='max_users') @click.option('-n', '--domain_name')
@manager.option('-a', '--max_aliases', dest='max_aliases') @click.option('-u', '--max_users')
@manager.option('-q', '--max_quota_bytes', dest='max_quota_bytes') @click.option('-a', '--max_aliases')
@click.option('-q', '--max_quota_bytes')
@flask_cli.with_appcontext
def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0): def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0):
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
@ -72,15 +99,17 @@ def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0):
db.session.commit() db.session.commit()
@manager.command @mailu.command()
def user_import(localpart, domain_name, password_hash, @click.argument('localpart')
hash_scheme=app.config['PASSWORD_SCHEME']): @click.argument('domain_name')
""" Import a user along with password hash. Available hashes: @click.argument('password_hash')
'SHA512-CRYPT' @click.argument('hash_scheme')
'SHA256-CRYPT' @flask_cli.with_appcontext
'MD5-CRYPT' def user_import(localpart, domain_name, password_hash, hash_scheme = None):
'CRYPT' """ Import a user along with password hash.
""" """
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
@ -95,7 +124,10 @@ def user_import(localpart, domain_name, password_hash,
db.session.commit() db.session.commit()
@manager.command @mailu.command()
@click.option('-v', '--verbose')
@click.option('-d', '--delete_objects')
@flask_cli.with_appcontext
def config_update(verbose=False, delete_objects=False): def config_update(verbose=False, delete_objects=False):
"""sync configuration with data from YAML-formatted stdin""" """sync configuration with data from YAML-formatted stdin"""
import yaml import yaml
@ -234,7 +266,9 @@ def config_update(verbose=False, delete_objects=False):
db.session.commit() db.session.commit()
@manager.command @mailu.command()
@click.argument('email')
@flask_cli.with_appcontext
def user_delete(email): def user_delete(email):
"""delete user""" """delete user"""
user = models.User.query.get(email) user = models.User.query.get(email)
@ -243,7 +277,9 @@ def user_delete(email):
db.session.commit() db.session.commit()
@manager.command @mailu.command()
@click.argument('email')
@flask_cli.with_appcontext
def alias_delete(email): def alias_delete(email):
"""delete alias""" """delete alias"""
alias = models.Alias.query.get(email) alias = models.Alias.query.get(email)
@ -252,7 +288,11 @@ def alias_delete(email):
db.session.commit() db.session.commit()
@manager.command @mailu.command()
@click.argument('localpart')
@click.argument('domain_name')
@click.argument('destination')
@flask_cli.with_appcontext
def alias(localpart, domain_name, destination): def alias(localpart, domain_name, destination):
""" Create an alias """ Create an alias
""" """
@ -269,24 +309,31 @@ def alias(localpart, domain_name, destination):
db.session.add(alias) db.session.add(alias)
db.session.commit() db.session.commit()
# Set limits to a domain
@mailu.command()
@manager.command @click.argument('domain_name')
@click.argument('max_users')
@click.argument('max_aliases')
@click.argument('max_quota_bytes')
@flask_cli.with_appcontext
def setlimits(domain_name, max_users, max_aliases, max_quota_bytes): def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
""" Set domain limits
"""
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
domain.max_users = max_users domain.max_users = max_users
domain.max_aliases = max_aliases domain.max_aliases = max_aliases
domain.max_quota_bytes = max_quota_bytes domain.max_quota_bytes = max_quota_bytes
db.session.add(domain) db.session.add(domain)
db.session.commit() db.session.commit()
# Make the user manager of a domain
@mailu.command()
@manager.command @click.argument('domain_name')
@click.argument('user_name')
@flask_cli.with_appcontext
def setmanager(domain_name, user_name='manager'): def setmanager(domain_name, user_name='manager'):
""" Make a user manager of a domain
"""
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
manageruser = models.User.query.get(user_name + '@' + domain_name) manageruser = models.User.query.get(user_name + '@' + domain_name)
domain.managers.append(manageruser) domain.managers.append(manageruser)
@ -294,5 +341,5 @@ def setmanager(domain_name, user_name='manager'):
db.session.commit() db.session.commit()
if __name__ == "__main__": if __name__ == '__main__':
manager.run() cli()

@ -1,10 +1,12 @@
from mailu import app, db, dkim, login_manager from mailu import dkim
from sqlalchemy.ext import declarative from sqlalchemy.ext import declarative
from passlib import context, hash from passlib import context, hash
from datetime import datetime, date from datetime import datetime, date
from email.mime import text from email.mime import text
from flask import current_app as app
import flask_sqlalchemy
import sqlalchemy import sqlalchemy
import re import re
import time import time
@ -15,6 +17,9 @@ import idna
import dns import dns
db = flask_sqlalchemy.SQLAlchemy()
class IdnaDomain(db.TypeDecorator): class IdnaDomain(db.TypeDecorator):
""" Stores a Unicode string in it's IDNA representation (ASCII only) """ Stores a Unicode string in it's IDNA representation (ASCII only)
""" """
@ -67,7 +72,28 @@ class CommaSeparatedList(db.TypeDecorator):
return ",".join(value) return ",".join(value)
def process_result_value(self, value, dialect): def process_result_value(self, value, dialect):
return filter(bool, value.split(",")) return filter(bool, value.split(",")) if value else []
class JSONEncoded(db.TypeDecorator):
"""Represents an immutable structure as a json-encoded string.
"""
impl = db.String
def process_bind_param(self, value, dialect):
return json.dumps(value) if value else None
def process_result_value(self, value, dialect):
return json.loads(value) if value else None
class Config(db.Model):
""" In-database configuration values
"""
name = db.Column(db.String(255), primary_key=True, nullable=False)
value = db.Column(JSONEncoded)
# Many-to-many association table for domain managers # Many-to-many association table for domain managers
@ -224,6 +250,28 @@ class Email(object):
msg['To'] = to_address msg['To'] = to_address
smtp.sendmail(from_address, [to_address], msg.as_string()) smtp.sendmail(from_address, [to_address], msg.as_string())
@classmethod
def resolve_domain(cls, email):
localpart, domain_name = email.split('@', 1) if '@' in email else (None, email)
alternative = Alternative.query.get(domain_name)
if alternative:
domain_name = alternative.domain_name
return (localpart, domain_name)
@classmethod
def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False):
alias = Alias.resolve(localpart, domain_name)
if alias:
return alias.destination
user = User.query.get('{}@{}'.format(localpart, domain_name))
if user:
if user.forward_enabled:
destination = user.forward_destination
if user.forward_keep or ignore_forward_keep:
destination.append(user.email)
else:
destination = [user.email]
return destination
def __str__(self): def __str__(self):
return self.email return self.email
@ -248,7 +296,7 @@ class User(Base, Email):
# Filters # Filters
forward_enabled = db.Column(db.Boolean(), nullable=False, default=False) forward_enabled = db.Column(db.Boolean(), nullable=False, default=False)
forward_destination = db.Column(db.String(255), nullable=True, default=None) forward_destination = db.Column(CommaSeparatedList(), nullable=True, default=[])
forward_keep = db.Column(db.Boolean(), nullable=False, default=True) forward_keep = db.Column(db.Boolean(), nullable=False, default=True)
reply_enabled = db.Column(db.Boolean(), nullable=False, default=False) reply_enabled = db.Column(db.Boolean(), nullable=False, default=False)
reply_subject = db.Column(db.String(255), nullable=True, default=None) reply_subject = db.Column(db.String(255), nullable=True, default=None)
@ -296,13 +344,15 @@ class User(Base, Email):
'SHA256-CRYPT': "sha256_crypt", 'SHA256-CRYPT': "sha256_crypt",
'MD5-CRYPT': "md5_crypt", 'MD5-CRYPT': "md5_crypt",
'CRYPT': "des_crypt"} 'CRYPT': "des_crypt"}
pw_context = context.CryptContext(
schemes = scheme_dict.values(), def get_password_context(self):
default=scheme_dict[app.config['PASSWORD_SCHEME']], return context.CryptContext(
) schemes=self.scheme_dict.values(),
default=self.scheme_dict[app.config['PASSWORD_SCHEME']],
)
def check_password(self, password): def check_password(self, password):
context = User.pw_context context = self.get_password_context()
reference = re.match('({[^}]+})?(.*)', self.password).group(2) reference = re.match('({[^}]+})?(.*)', self.password).group(2)
result = context.verify(password, reference) result = context.verify(password, reference)
if result and context.identify(reference) != context.default_scheme(): if result and context.identify(reference) != context.default_scheme():
@ -311,15 +361,17 @@ class User(Base, Email):
db.session.commit() db.session.commit()
return result return result
def set_password(self, password, hash_scheme=app.config['PASSWORD_SCHEME'], raw=False): def set_password(self, password, hash_scheme=None, raw=False):
"""Set password for user with specified encryption scheme """Set password for user with specified encryption scheme
@password: plain text password to encrypt (if raw == True the hash itself) @password: plain text password to encrypt (if raw == True the hash itself)
""" """
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
# for the list of hash schemes see https://wiki2.dovecot.org/Authentication/PasswordSchemes # for the list of hash schemes see https://wiki2.dovecot.org/Authentication/PasswordSchemes
if raw: if raw:
self.password = '{'+hash_scheme+'}' + password self.password = '{'+hash_scheme+'}' + password
else: else:
self.password = '{'+hash_scheme+'}' + User.pw_context.encrypt(password, self.scheme_dict[hash_scheme]) self.password = '{'+hash_scheme+'}' + self.get_password_context().encrypt(password, self.scheme_dict[hash_scheme])
def get_managed_domains(self): def get_managed_domains(self):
if self.global_admin: if self.global_admin:
@ -340,13 +392,15 @@ class User(Base, Email):
self.sendmail(app.config["WELCOME_SUBJECT"], self.sendmail(app.config["WELCOME_SUBJECT"],
app.config["WELCOME_BODY"]) app.config["WELCOME_BODY"])
@classmethod
def get(cls, email):
return cls.query.get(email)
@classmethod @classmethod
def login(cls, email, password): def login(cls, email, password):
user = cls.query.get(email) user = cls.query.get(email)
return user if (user and user.enabled and user.check_password(password)) else None return user if (user and user.enabled and user.check_password(password)) else None
login_manager.user_loader(User.query.get)
class Alias(Base, Email): class Alias(Base, Email):
""" An alias is an email address that redirects to some destination. """ An alias is an email address that redirects to some destination.

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import forms from mailu.ui import forms
import flask import flask

@ -90,9 +90,10 @@ class UserSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
captcha = flask_wtf.RecaptchaField()
submit = fields.SubmitField(_('Sign up')) submit = fields.SubmitField(_('Sign up'))
class UserSignupFormCaptcha(UserSignupForm):
captcha = flask_wtf.RecaptchaField()
class UserSettingsForm(flask_wtf.FlaskForm): class UserSettingsForm(flask_wtf.FlaskForm):
displayed_name = fields.StringField(_('Displayed name')) displayed_name = fields.StringField(_('Displayed name'))

@ -14,7 +14,9 @@
{% call macros.box() %} {% call macros.box() %}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{{ macros.form_field(form.captcha) }} {% if form.captcha %}
{{ macros.form_field(form.captcha) }}
{% endif %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
{% endcall %} {% endcall %}
</form> </form>

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
@ -25,7 +25,7 @@ def admin_create():
user = models.User.query.get(form.admin.data) user = models.User.query.get(form.admin.data)
if user: if user:
user.global_admin = True user.global_admin = True
db.session.commit() models.db.session.commit()
flask.flash('User %s is now admin' % user) flask.flash('User %s is now admin' % user)
return flask.redirect(flask.url_for('.admin_list')) return flask.redirect(flask.url_for('.admin_list'))
else: else:
@ -40,7 +40,7 @@ def admin_delete(admin):
user = models.User.query.get(admin) user = models.User.query.get(admin)
if user: if user:
user.global_admin = False user.global_admin = False
db.session.commit() models.db.session.commit()
flask.flash('User %s is no longer admin' % user) flask.flash('User %s is no longer admin' % user)
return flask.redirect(flask.url_for('.admin_list')) return flask.redirect(flask.url_for('.admin_list'))
else: else:

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
@ -27,8 +27,8 @@ def alias_create(domain_name):
else: else:
alias = models.Alias(domain=domain) alias = models.Alias(domain=domain)
form.populate_obj(alias) form.populate_obj(alias)
db.session.add(alias) models.db.session.add(alias)
db.session.commit() models.db.session.commit()
flask.flash('Alias %s created' % alias) flask.flash('Alias %s created' % alias)
return flask.redirect( return flask.redirect(
flask.url_for('.alias_list', domain_name=domain.name)) flask.url_for('.alias_list', domain_name=domain.name))
@ -45,7 +45,7 @@ def alias_edit(alias):
form.localpart.validators = [] form.localpart.validators = []
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(alias) form.populate_obj(alias)
db.session.commit() models.db.session.commit()
flask.flash('Alias %s updated' % alias) flask.flash('Alias %s updated' % alias)
return flask.redirect( return flask.redirect(
flask.url_for('.alias_list', domain_name=alias.domain.name)) flask.url_for('.alias_list', domain_name=alias.domain.name))
@ -59,8 +59,8 @@ def alias_edit(alias):
def alias_delete(alias): def alias_delete(alias):
alias = models.Alias.query.get(alias) or flask.abort(404) alias = models.Alias.query.get(alias) or flask.abort(404)
domain = alias.domain domain = alias.domain
db.session.delete(alias) models.db.session.delete(alias)
db.session.commit() models.db.session.commit()
flask.flash('Alias %s deleted' % alias) flask.flash('Alias %s deleted' % alias)
return flask.redirect( return flask.redirect(
flask.url_for('.alias_list', domain_name=domain.name)) flask.url_for('.alias_list', domain_name=domain.name))

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
@ -26,8 +26,8 @@ def alternative_create(domain_name):
else: else:
alternative = models.Alternative(domain=domain) alternative = models.Alternative(domain=domain)
form.populate_obj(alternative) form.populate_obj(alternative)
db.session.add(alternative) models.db.session.add(alternative)
db.session.commit() models.db.session.commit()
flask.flash('Alternative domain %s created' % alternative) flask.flash('Alternative domain %s created' % alternative)
return flask.redirect( return flask.redirect(
flask.url_for('.alternative_list', domain_name=domain.name)) flask.url_for('.alternative_list', domain_name=domain.name))
@ -41,8 +41,8 @@ def alternative_create(domain_name):
def alternative_delete(alternative): def alternative_delete(alternative):
alternative = models.Alternative.query.get(alternative) or flask.abort(404) alternative = models.Alternative.query.get(alternative) or flask.abort(404)
domain = alternative.domain domain = alternative.domain
db.session.delete(alternative) models.db.session.delete(alternative)
db.session.commit() models.db.session.commit()
flask.flash('Alternative %s deleted' % alternative) flask.flash('Alternative %s deleted' % alternative)
return flask.redirect( return flask.redirect(
flask.url_for('.alternative_list', domain_name=domain.name)) flask.url_for('.alternative_list', domain_name=domain.name))

@ -1,11 +1,9 @@
from mailu import dockercli, app, db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
import flask_login import flask_login
from urllib import parse
@ui.route('/', methods=["GET"]) @ui.route('/', methods=["GET"])
@access.authenticated @access.authenticated

@ -1,5 +1,6 @@
from mailu import app, db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
from flask import current_app as app
import flask import flask
import flask_login import flask_login
@ -26,8 +27,8 @@ def domain_create():
else: else:
domain = models.Domain() domain = models.Domain()
form.populate_obj(domain) form.populate_obj(domain)
db.session.add(domain) models.db.session.add(domain)
db.session.commit() models.db.session.commit()
flask.flash('Domain %s created' % domain) flask.flash('Domain %s created' % domain)
return flask.redirect(flask.url_for('.domain_list')) return flask.redirect(flask.url_for('.domain_list'))
return flask.render_template('domain/create.html', form=form) return flask.render_template('domain/create.html', form=form)
@ -42,7 +43,7 @@ def domain_edit(domain_name):
form.name.validators = [] form.name.validators = []
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(domain) form.populate_obj(domain)
db.session.commit() models.db.session.commit()
flask.flash('Domain %s saved' % domain) flask.flash('Domain %s saved' % domain)
return flask.redirect(flask.url_for('.domain_list')) return flask.redirect(flask.url_for('.domain_list'))
return flask.render_template('domain/edit.html', form=form, return flask.render_template('domain/edit.html', form=form,
@ -54,8 +55,8 @@ def domain_edit(domain_name):
@access.confirmation_required("delete {domain_name}") @access.confirmation_required("delete {domain_name}")
def domain_delete(domain_name): def domain_delete(domain_name):
domain = models.Domain.query.get(domain_name) or flask.abort(404) domain = models.Domain.query.get(domain_name) or flask.abort(404)
db.session.delete(domain) models.db.session.delete(domain)
db.session.commit() models.db.session.commit()
flask.flash('Domain %s deleted' % domain) flask.flash('Domain %s deleted' % domain)
return flask.redirect(flask.url_for('.domain_list')) return flask.redirect(flask.url_for('.domain_list'))
@ -99,7 +100,7 @@ def domain_signup(domain_name=None):
domain.max_users = 10 domain.max_users = 10
domain.max_aliases = 10 domain.max_aliases = 10
if domain.check_mx(): if domain.check_mx():
db.session.add(domain) models.db.session.add(domain)
if flask_login.current_user.is_authenticated: if flask_login.current_user.is_authenticated:
user = models.User.query.get(flask_login.current_user.email) user = models.User.query.get(flask_login.current_user.email)
else: else:
@ -108,9 +109,9 @@ def domain_signup(domain_name=None):
form.populate_obj(user) form.populate_obj(user)
user.set_password(form.pw.data) user.set_password(form.pw.data)
user.quota_bytes = domain.max_quota_bytes user.quota_bytes = domain.max_quota_bytes
db.session.add(user) models.db.session.add(user)
domain.managers.append(user) domain.managers.append(user)
db.session.commit() models.db.session.commit()
flask.flash('Domain %s created' % domain) flask.flash('Domain %s created' % domain)
return flask.redirect(flask.url_for('.domain_list')) return flask.redirect(flask.url_for('.domain_list'))
else: else:

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
@ -24,8 +24,8 @@ def fetch_create(user_email):
if form.validate_on_submit(): if form.validate_on_submit():
fetch = models.Fetch(user=user) fetch = models.Fetch(user=user)
form.populate_obj(fetch) form.populate_obj(fetch)
db.session.add(fetch) models.db.session.add(fetch)
db.session.commit() models.db.session.commit()
flask.flash('Fetch configuration created') flask.flash('Fetch configuration created')
return flask.redirect( return flask.redirect(
flask.url_for('.fetch_list', user_email=user.email)) flask.url_for('.fetch_list', user_email=user.email))
@ -39,7 +39,7 @@ def fetch_edit(fetch_id):
form = forms.FetchForm(obj=fetch) form = forms.FetchForm(obj=fetch)
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(fetch) form.populate_obj(fetch)
db.session.commit() models.db.session.commit()
flask.flash('Fetch configuration updated') flask.flash('Fetch configuration updated')
return flask.redirect( return flask.redirect(
flask.url_for('.fetch_list', user_email=fetch.user.email)) flask.url_for('.fetch_list', user_email=fetch.user.email))
@ -53,8 +53,8 @@ def fetch_edit(fetch_id):
def fetch_delete(fetch_id): def fetch_delete(fetch_id):
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404) fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
user = fetch.user user = fetch.user
db.session.delete(fetch) models.db.session.delete(fetch)
db.session.commit() models.db.session.commit()
flask.flash('Fetch configuration delete') flask.flash('Fetch configuration delete')
return flask.redirect( return flask.redirect(
flask.url_for('.fetch_list', user_email=user.email)) flask.url_for('.fetch_list', user_email=user.email))

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
@ -30,7 +30,7 @@ def manager_create(domain_name):
flask.flash('User %s is already manager' % user, 'error') flask.flash('User %s is already manager' % user, 'error')
else: else:
domain.managers.append(user) domain.managers.append(user)
db.session.commit() models.db.session.commit()
flask.flash('User %s can now manage %s' % (user, domain.name)) flask.flash('User %s can now manage %s' % (user, domain.name))
return flask.redirect( return flask.redirect(
flask.url_for('.manager_list', domain_name=domain.name)) flask.url_for('.manager_list', domain_name=domain.name))
@ -46,7 +46,7 @@ def manager_delete(domain_name, user_email):
user = models.User.query.get(user_email) or flask.abort(404) user = models.User.query.get(user_email) or flask.abort(404)
if user in domain.managers: if user in domain.managers:
domain.managers.remove(user) domain.managers.remove(user)
db.session.commit() models.db.session.commit()
flask.flash('User %s can no longer manager %s' % (user, domain)) flask.flash('User %s can no longer manager %s' % (user, domain))
else: else:
flask.flash('User %s is not manager' % user, 'error') flask.flash('User %s is not manager' % user, 'error')

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask
@ -25,8 +25,8 @@ def relay_create():
else: else:
relay = models.Relay() relay = models.Relay()
form.populate_obj(relay) form.populate_obj(relay)
db.session.add(relay) models.db.session.add(relay)
db.session.commit() models.db.session.commit()
flask.flash('Relayed domain %s created' % relay) flask.flash('Relayed domain %s created' % relay)
return flask.redirect(flask.url_for('.relay_list')) return flask.redirect(flask.url_for('.relay_list'))
return flask.render_template('relay/create.html', form=form) return flask.render_template('relay/create.html', form=form)
@ -41,7 +41,7 @@ def relay_edit(relay_name):
form.name.validators = [] form.name.validators = []
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(relay) form.populate_obj(relay)
db.session.commit() models.db.session.commit()
flask.flash('Relayed domain %s saved' % relay) flask.flash('Relayed domain %s saved' % relay)
return flask.redirect(flask.url_for('.relay_list')) return flask.redirect(flask.url_for('.relay_list'))
return flask.render_template('relay/edit.html', form=form, return flask.render_template('relay/edit.html', form=form,
@ -53,8 +53,8 @@ def relay_edit(relay_name):
@access.confirmation_required("delete {relay_name}") @access.confirmation_required("delete {relay_name}")
def relay_delete(relay_name): def relay_delete(relay_name):
relay = models.Relay.query.get(relay_name) or flask.abort(404) relay = models.Relay.query.get(relay_name) or flask.abort(404)
db.session.delete(relay) models.db.session.delete(relay)
db.session.commit() models.db.session.commit()
flask.flash('Relayed domain %s deleted' % relay) flask.flash('Relayed domain %s deleted' % relay)
return flask.redirect(flask.url_for('.relay_list')) return flask.redirect(flask.url_for('.relay_list'))

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
from passlib import pwd from passlib import pwd
@ -32,8 +32,8 @@ def token_create(user_email):
token = models.Token(user=user) token = models.Token(user=user)
token.set_password(form.raw_password.data) token.set_password(form.raw_password.data)
form.populate_obj(token) form.populate_obj(token)
db.session.add(token) models.db.session.add(token)
db.session.commit() models.db.session.commit()
flask.flash('Authentication token created') flask.flash('Authentication token created')
return flask.redirect( return flask.redirect(
flask.url_for('.token_list', user_email=user.email)) flask.url_for('.token_list', user_email=user.email))
@ -46,8 +46,8 @@ def token_create(user_email):
def token_delete(token_id): def token_delete(token_id):
token = models.Token.query.get(token_id) or flask.abort(404) token = models.Token.query.get(token_id) or flask.abort(404)
user = token.user user = token.user
db.session.delete(token) models.db.session.delete(token)
db.session.commit() models.db.session.commit()
flask.flash('Authentication token deleted') flask.flash('Authentication token deleted')
return flask.redirect( return flask.redirect(
flask.url_for('.token_list', user_email=user.email)) flask.url_for('.token_list', user_email=user.email))

@ -1,5 +1,6 @@
from mailu import db, models, app from mailu import models
from mailu.ui import ui, access, forms from mailu.ui import ui, access, forms
from flask import current_app as app
import flask import flask
import flask_login import flask_login
@ -33,8 +34,8 @@ def user_create(domain_name):
user = models.User(domain=domain) user = models.User(domain=domain)
form.populate_obj(user) form.populate_obj(user)
user.set_password(form.pw.data) user.set_password(form.pw.data)
db.session.add(user) models.db.session.add(user)
db.session.commit() models.db.session.commit()
user.send_welcome() user.send_welcome()
flask.flash('User %s created' % user) flask.flash('User %s created' % user)
return flask.redirect( return flask.redirect(
@ -63,7 +64,7 @@ def user_edit(user_email):
form.populate_obj(user) form.populate_obj(user)
if form.pw.data: if form.pw.data:
user.set_password(form.pw.data) user.set_password(form.pw.data)
db.session.commit() models.db.session.commit()
flask.flash('User %s updated' % user) flask.flash('User %s updated' % user)
return flask.redirect( return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name)) flask.url_for('.user_list', domain_name=user.domain.name))
@ -77,8 +78,8 @@ def user_edit(user_email):
def user_delete(user_email): def user_delete(user_email):
user = models.User.query.get(user_email) or flask.abort(404) user = models.User.query.get(user_email) or flask.abort(404)
domain = user.domain domain = user.domain
db.session.delete(user) models.db.session.delete(user)
db.session.commit() models.db.session.commit()
flask.flash('User %s deleted' % user) flask.flash('User %s deleted' % user)
return flask.redirect( return flask.redirect(
flask.url_for('.user_list', domain_name=domain.name)) flask.url_for('.user_list', domain_name=domain.name))
@ -93,7 +94,7 @@ def user_settings(user_email):
form = forms.UserSettingsForm(obj=user) form = forms.UserSettingsForm(obj=user)
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(user) form.populate_obj(user)
db.session.commit() models.db.session.commit()
flask.flash('Settings updated for %s' % user) flask.flash('Settings updated for %s' % user)
if user_email: if user_email:
return flask.redirect( return flask.redirect(
@ -113,7 +114,7 @@ def user_password(user_email):
flask.flash('Passwords do not match', 'error') flask.flash('Passwords do not match', 'error')
else: else:
user.set_password(form.pw.data) user.set_password(form.pw.data)
db.session.commit() models.db.session.commit()
flask.flash('Password updated for %s' % user) flask.flash('Password updated for %s' % user)
if user_email: if user_email:
return flask.redirect(flask.url_for('.user_list', return flask.redirect(flask.url_for('.user_list',
@ -130,7 +131,7 @@ def user_forward(user_email):
form = forms.UserForwardForm(obj=user) form = forms.UserForwardForm(obj=user)
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(user) form.populate_obj(user)
db.session.commit() models.db.session.commit()
flask.flash('Forward destination updated for %s' % user) flask.flash('Forward destination updated for %s' % user)
if user_email: if user_email:
return flask.redirect( return flask.redirect(
@ -147,7 +148,7 @@ def user_reply(user_email):
form = forms.UserReplyForm(obj=user) form = forms.UserReplyForm(obj=user)
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(user) form.populate_obj(user)
db.session.commit() models.db.session.commit()
flask.flash('Auto-reply message updated for %s' % user) flask.flash('Auto-reply message updated for %s' % user)
if user_email: if user_email:
return flask.redirect( return flask.redirect(
@ -170,7 +171,11 @@ def user_signup(domain_name=None):
available_domains=available_domains) available_domains=available_domains)
domain = available_domains.get(domain_name) or flask.abort(404) domain = available_domains.get(domain_name) or flask.abort(404)
quota_bytes = domain.max_quota_bytes or app.config['DEFAULT_QUOTA'] quota_bytes = domain.max_quota_bytes or app.config['DEFAULT_QUOTA']
form = forms.UserSignupForm() if app.config['RECAPTCHA_PUBLIC_KEY'] == "" or app.config['RECAPTCHA_PRIVATE_KEY'] == "":
form = forms.UserSignupForm()
else:
form = forms.UserSignupFormCaptcha()
if form.validate_on_submit(): if form.validate_on_submit():
if domain.has_email(form.localpart.data): if domain.has_email(form.localpart.data):
flask.flash('Email is already used', 'error') flask.flash('Email is already used', 'error')
@ -179,8 +184,8 @@ def user_signup(domain_name=None):
form.populate_obj(user) form.populate_obj(user)
user.set_password(form.pw.data) user.set_password(form.pw.data)
user.quota_bytes = quota_bytes user.quota_bytes = quota_bytes
db.session.add(user) models.db.session.add(user)
db.session.commit() models.db.session.commit()
user.send_welcome() user.send_welcome()
flask.flash('Successfully signed up %s' % user) flask.flash('Successfully signed up %s' % user)
return flask.redirect(flask.url_for('.index')) return flask.redirect(flask.url_for('.index'))

@ -0,0 +1,53 @@
from mailu import models
import flask
import flask_login
import flask_script
import flask_migrate
import flask_babel
import flask_limiter
from werkzeug.contrib import fixers
# Login configuration
login = flask_login.LoginManager()
login.login_view = "ui.login"
@login.unauthorized_handler
def handle_needs_login():
return flask.redirect(
flask.url_for('ui.login', next=flask.request.endpoint)
)
# Request rate limitation
limiter = flask_limiter.Limiter(key_func=lambda: current_user.username)
# Application translation
babel = flask_babel.Babel()
@babel.localeselector
def get_locale():
translations = list(map(str, babel.list_translations()))
return flask.request.accept_languages.best_match(translations)
# Proxy fixer
class PrefixMiddleware(object):
def __call__(self, environ, start_response):
prefix = environ.get('HTTP_X_FORWARDED_PREFIX', '')
if prefix:
environ['SCRIPT_NAME'] = prefix
return self.app(environ, start_response)
def init_app(self, app):
self.app = fixers.ProxyFix(app.wsgi_app)
app.wsgi_app = self
proxy = PrefixMiddleware()
# Data migrate
migrate = flask_migrate.Migrate()

@ -13,8 +13,6 @@ down_revision = '2335c80a6bc3'
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from mailu import app
fetch_table = sa.Table( fetch_table = sa.Table(
'fetch', 'fetch',
@ -24,13 +22,7 @@ fetch_table = sa.Table(
def upgrade(): def upgrade():
connection = op.get_bind()
op.add_column('fetch', sa.Column('keep', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false())) op.add_column('fetch', sa.Column('keep', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
# also apply the current config value if set
if app.config.get("FETCHMAIL_KEEP", "False") == "True":
connection.execute(
fetch_table.update().values(keep=True)
)
def downgrade(): def downgrade():

@ -0,0 +1,25 @@
""" Add a configuration table
Revision ID: cd79ed46d9c2
Revises: 25fd6c7bcb4a
Create Date: 2018-10-17 21:44:48.924921
"""
revision = 'cd79ed46d9c2'
down_revision = '3b281286c7bd'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('config',
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('value', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('name')
)
def downgrade():
op.drop_table('config')

@ -1,53 +1,46 @@
alembic==0.9.9 alembic==1.0.2
asn1crypto==0.24.0 asn1crypto==0.24.0
Babel==2.5.3 Babel==2.6.0
bcrypt==3.1.4 bcrypt==3.1.4
blinker==1.4 blinker==1.4
certifi==2018.4.16
cffi==1.11.5 cffi==1.11.5
chardet==3.0.4 Click==7.0
click==6.7 cryptography==2.3.1
cryptography==2.2.2
decorator==4.3.0 decorator==4.3.0
dnspython==1.15.0 dnspython==1.15.0
docker-py==1.10.6 dominate==2.3.4
docker-pycreds==0.2.2 Flask==1.0.2
dominate==2.3.1 Flask-Babel==0.12.2
Flask==0.12.2
Flask-Babel==0.11.2
Flask-Bootstrap==3.3.7.1 Flask-Bootstrap==3.3.7.1
Flask-DebugToolbar==0.10.1 Flask-DebugToolbar==0.10.1
Flask-Limiter==1.0.1 Flask-Limiter==1.0.1
Flask-Login==0.4.1 Flask-Login==0.4.1
Flask-Migrate==2.1.1 Flask-Migrate==2.3.0
Flask-Script==2.0.6 Flask-Script==2.0.6
Flask-SQLAlchemy==2.3.2 Flask-SQLAlchemy==2.3.2
Flask-WTF==0.14.2 Flask-WTF==0.14.2
gunicorn==19.7.1 gunicorn==19.9.0
idna==2.6 idna==2.7
infinity==1.4 infinity==1.4
intervals==0.8.1 intervals==0.8.1
itsdangerous==0.24 itsdangerous==1.1.0
Jinja2==2.10 Jinja2==2.10
limits==1.3 limits==1.3
Mako==1.0.7 Mako==1.0.7
MarkupSafe==1.0 MarkupSafe==1.1.0
passlib==1.7.1 passlib==1.7.1
pycparser==2.18 pycparser==2.19
pyOpenSSL==17.5.0 pyOpenSSL==18.0.0
python-dateutil==2.7.2 python-dateutil==2.7.5
python-editor==1.0.3 python-editor==1.0.3
pytz==2018.4 pytz==2018.7
PyYAML==3.12 PyYAML==3.13
redis==2.10.6 redis==2.10.6
requests==2.18.4
six==1.11.0 six==1.11.0
SQLAlchemy==1.2.6 SQLAlchemy==1.2.13
tabulate==0.8.2 tabulate==0.8.2
urllib3==1.22 validators==0.12.2
validators==0.12.1
visitor==0.1.3 visitor==0.1.3
websocket-client==0.47.0
Werkzeug==0.14.1 Werkzeug==0.14.1
WTForms==2.1 WTForms==2.2.1
WTForms-Components==0.10.3 WTForms-Components==0.10.3

@ -12,7 +12,6 @@ redis
WTForms-Components WTForms-Components
passlib passlib
gunicorn gunicorn
docker-py
tabulate tabulate
PyYAML PyYAML
PyOpenSSL PyOpenSSL

@ -1,7 +0,0 @@
import os
if __name__ == "__main__":
os.environ["DEBUG"] = "True"
from mailu import app
app.run()

@ -2,6 +2,6 @@
import os import os
os.system("python3 manage.py advertise") os.system("flask mailu advertise")
os.system("python3 manage.py db upgrade") os.system("flask db upgrade")
os.system("gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload mailu:app") os.system("gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload 'mailu:create_app()'")

@ -250,7 +250,7 @@ mail {
listen 465 ssl; listen 465 ssl;
listen [::]:465 ssl; listen [::]:465 ssl;
protocol smtp; protocol smtp;
smtp_auth plain; smtp_auth plain login;
} }
server { server {

@ -78,14 +78,14 @@ lmtp_host_lookup = native
smtpd_delay_reject = yes smtpd_delay_reject = yes
# Allowed senders are: the user or one of the alias destinations # Allowed senders are: the user or one of the alias destinations
smtpd_sender_login_maps = $virtual_alias_maps smtpd_sender_login_maps = ${podop}senderlogin
# Restrictions for incoming SMTP, other restrictions are applied in master.cf # Restrictions for incoming SMTP, other restrictions are applied in master.cf
smtpd_helo_required = yes smtpd_helo_required = yes
smtpd_client_restrictions = smtpd_client_restrictions =
permit_mynetworks, permit_mynetworks,
check_sender_access ${podop}sender, check_sender_access ${podop}senderaccess,
reject_non_fqdn_sender, reject_non_fqdn_sender,
reject_unknown_sender_domain, reject_unknown_sender_domain,
reject_unknown_recipient_domain, reject_unknown_recipient_domain,

@ -19,7 +19,8 @@ def start_podop():
("alias", "url", "http://admin/internal/postfix/alias/§"), ("alias", "url", "http://admin/internal/postfix/alias/§"),
("domain", "url", "http://admin/internal/postfix/domain/§"), ("domain", "url", "http://admin/internal/postfix/domain/§"),
("mailbox", "url", "http://admin/internal/postfix/mailbox/§"), ("mailbox", "url", "http://admin/internal/postfix/mailbox/§"),
("sender", "url", "http://admin/internal/postfix/sender/§") ("senderaccess", "url", "http://admin/internal/postfix/sender/access/§"),
("senderlogin", "url", "http://admin/internal/postfix/sender/login/§")
]) ])
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))

@ -15,7 +15,7 @@ alias
.. code-block:: bash .. code-block:: bash
docker-compose run --rm admin python manage.py alias foo example.net "mail1@example.com,mail2@example.com" docker-compose exec admin flask mailu alias foo example.net "mail1@example.com,mail2@example.com"
alias_delete alias_delete
@ -23,14 +23,14 @@ alias_delete
.. code-block:: bash .. code-block:: bash
docker-compose run --rm admin python manage.py alias_delete foo@example.net docker-compose exec admin flask mailu alias_delete foo@example.net
user user
---- ----
.. code-block:: bash .. code-block:: bash
docker-compose run --rm admin python manage.py user --hash_scheme='SHA512-CRYPT' myuser example.net 'password123' docker-compose exec admin flask mailu user --hash_scheme='SHA512-CRYPT' myuser example.net 'password123'
user_import user_import
----------- -----------
@ -39,14 +39,14 @@ primary difference with simple `user` command is that password is being imported
.. code-block:: bash .. code-block:: bash
docker-compose run --rm admin python manage.py user_import --hash_scheme='SHA512-CRYPT' myuser example.net '$6$51ebe0cb9f1dab48effa2a0ad8660cb489b445936b9ffd812a0b8f46bca66dd549fea530ce' docker-compose run --rm admin python manage.py user --hash_scheme='SHA512-CRYPT' myuser example.net '$6$51ebe0cb9f1dab48effa2a0ad8660cb489b445936b9ffd812a0b8f46bca66dd549fea530ce'
user_delete user_delete
------------ ------------
.. code-block:: bash .. code-block:: bash
docker-compose run --rm admin python manage.py user_delete foo@example.net docker-compose exec admin flask mailu user_delete foo@example.net
config_update config_update
------------- -------------
@ -55,7 +55,7 @@ The sole purpose of this command is for importing users/aliases in bulk and sync
.. code-block:: bash .. code-block:: bash
cat mail-config.yml | docker-compose run --rm admin python manage.py config_update --delete_objects cat mail-config.yml | docker-compose exec admin flask mailu config_update --delete_objects
where mail-config.yml looks like: where mail-config.yml looks like:

@ -61,6 +61,7 @@ ANTIVIRUS=none
# Message size limit in bytes # Message size limit in bytes
# Default: accept messages up to 50MB # Default: accept messages up to 50MB
# Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT=50000000 MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker # Networks granted relay permissions, make sure that you include your Docker

@ -151,6 +151,6 @@ Finally, you must create the initial admin user account:
.. code-block:: bash .. code-block:: bash
docker-compose run --rm admin python manage.py admin root example.net password docker-compose exec admin flask mailu admin me example.net password
This will create a user named ``root@example.net`` with password ``password`` and administration privileges. Connect to the Web admin interface and change the password to a strong one. This will create a user named ``me@example.net`` with password ``password`` and administration privileges. Connect to the Web admin interface and change the password to a strong one.

@ -17,7 +17,7 @@ migration script:
.. code-block:: bash .. code-block:: bash
python manage.py db migrate flask db migrate
This will generate a new script in ``migrations/versions`` that you must review This will generate a new script in ``migrations/versions`` that you must review
before adding it for commit. before adding it for commit.
@ -54,7 +54,7 @@ At that point, to start working on the changed database structure, you will need
.. code-block:: bash .. code-block:: bash
python manage.py db upgrade flask db upgrade
If any error arises, restore the backup, fix the migration script and try again. If any error arises, restore the backup, fix the migration script and try again.

@ -12,6 +12,7 @@ COPY setup.py ./setup.py
COPY main.py ./main.py COPY main.py ./main.py
COPY flavors /data/master/flavors COPY flavors /data/master/flavors
COPY templates /data/master/templates COPY templates /data/master/templates
COPY static ./static
#RUN python setup.py https://github.com/mailu/mailu /data #RUN python setup.py https://github.com/mailu/mailu /data

@ -0,0 +1,59 @@
## Adding more flavors/steps
(Everything will go under setup/ directory - using Kubernetes flavor as example)
Until this point, the app is working as it follows:
- when accesing the setup page it will display the flavors selection step (`templates/steps/flavor.html`)
- after you choose your desired flavor it will iterare over the files in the flavor directory and building the page
(`templates/steps/config.html is general for all flavors`)
- when you complete all required fields and press "Setup Mailu" button it will redirect you to the setup page (`flavors/choosen-flavor/setup.html`)
To add a new flavor you need to create a directory under `templates/steps/` in which you are adding actual steps.
Eg: Adding a WIP step we'll create `templates/steps/kubernetes/wip.html`
*Note that wizard.html is iterating over files in this directory and building the page. Files are prefixed with a number for sorting purposes.*
wip.html will start with
```
{% call macros.panel("info", "Step X - Work in progress") %}
```
and end with
```
{% endcall %}
```
You store variable from front-page using the name attribute inside tag.
In the example below the string entered in the input field is stored in the variable `named var_test`
```
<input type="text" name="var_test">
```
In order to user the variable furter you use it like `{{ var_test }}`
In the setup page (`flavors/kubernetes/setup.html`) you cand add steps by importing macros
```
{% import "macros.html" as macros %}
```
and start and end every step with
```
{% call macros.panel("info", "Step X - Title") %}
-------------------
{% endcall %}
```
### Generating a file
Create the file template in `flavors/kubernetes/` (eg. file.txt) in which you save your variables
```
ROOT = {{ root }}
MY_VAR = {{ var_test }}
```
When you submit to Setup Mailu the file will be generated. In order to get the file add the following command to setup.html
```
<p>curl {{ url_for('.file', uid=uid, filepath='file.txt', _external=True) }} > file.txt</p>
```

@ -1,13 +1,13 @@
# This file is used to run the mailu/setup utility # This file is used to run the mailu/setup utility
version: '2' version: '3.6'
services: services:
redis: redis:
image: redis:alpine image: redis:alpine
setup: setup:
image: mailu/setup image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}setup:${MAILU_VERSION:-master}
ports: ports:
- "8000:80" - "8000:80"
build: . build: .

@ -10,13 +10,17 @@ services:
# External dependencies # External dependencies
redis: redis:
image: redis:alpine image: redis:alpine
restart: always
volumes: volumes:
- "{{ root }}/redis:/data" - "{{ root }}/redis:/data"
# Core services # Core services
front: front:
image: mailu/nginx:{{ version }} image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
logging:
driver: {{ log_driver or 'json-file' }}
ports: ports:
{% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %} {% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %}
{% if bind4 %} {% if bind4 %}
@ -41,7 +45,8 @@ services:
{% endif %} {% endif %}
admin: admin:
image: mailu/admin:{{ version }} image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
{% if not admin_enabled %} {% if not admin_enabled %}
ports: ports:
@ -54,7 +59,8 @@ services:
- redis - redis
imap: imap:
image: mailu/dovecot:{{ version }} image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/mail:/mail" - "{{ root }}/mail:/mail"
@ -63,7 +69,8 @@ services:
- front - front
smtp: smtp:
image: mailu/postfix:{{ version }} image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/overrides:/overrides" - "{{ root }}/overrides:/overrides"
@ -75,10 +82,9 @@ services:
- {{ dns }} - {{ dns }}
{% endif %} {% endif %}
# Optional services
{% if antispam_enabled %}
antispam: antispam:
image: mailu/rspamd:{{ version }} image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/filter:/var/lib/rspamd" - "{{ root }}/filter:/var/lib/rspamd"
@ -91,11 +97,12 @@ services:
dns: dns:
- {{ dns }} - {{ dns }}
{% endif %} {% endif %}
{% endif %}
# Optional services
{% if antivirus_enabled %} {% if antivirus_enabled %}
antivirus: antivirus:
image: mailu/clamav:{{ version }} image: ${DOCKER_ORG:-mailu}/clamav:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/filter:/data" - "{{ root }}/filter:/data"
@ -109,7 +116,8 @@ services:
{% if webdav_enabled %} {% if webdav_enabled %}
webdav: webdav:
image: mailu/radicale:{{ version }} image: ${DOCKER_ORG:-mailu}/radicale:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/dav:/data" - "{{ root }}/dav:/data"
@ -117,7 +125,8 @@ services:
{% if fetchmail_enabled %} {% if fetchmail_enabled %}
fetchmail: fetchmail:
image: mailu/fetchmail:{{ version }} image: ${DOCKER_ORG:-mailu}/fetchmail:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
{% if resolver_enabled %} {% if resolver_enabled %}
depends_on: depends_on:
@ -130,7 +139,8 @@ services:
# Webmail # Webmail
{% if webmail_type != 'none' %} {% if webmail_type != 'none' %}
webmail: webmail:
image: mailu/{{ webmail_type }}:{{ version }} image: ${DOCKER_ORG:-mailu}/{{ webmail_type }}:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/webmail:/data" - "{{ root }}/webmail:/data"

@ -73,6 +73,7 @@ ANTISPAM={{ antispam_enabled or 'none'}}
# Message size limit in bytes # Message size limit in bytes
# Default: accept messages up to 50MB # Default: accept messages up to 50MB
# Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }} MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }}
# Networks granted relay permissions, make sure that you include your Docker # Networks granted relay permissions, make sure that you include your Docker
@ -144,7 +145,7 @@ DOMAIN_REGISTRATION=true
# json-file (default) # json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration) # journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!) # syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
LOG_DRIVER={{ log_driver or 'json-file' }} # LOG_DRIVER={{ log_driver or 'json-file' }}
# Docker-compose project name, this will prepended to containers names. # Docker-compose project name, this will prepended to containers names.
COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }} COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }}

@ -28,15 +28,15 @@ files before going any further.</p>
{% call macros.panel("info", "Step 3 - Start the Compose project") %} {% call macros.panel("info", "Step 3 - Start the Compose project") %}
<p>To start your compose project, simply run the Docker Compose <code>up</code> <p>To start your compose project, simply run the Docker Compose <code>up</code>
command.</p> command using <code>-p mailu</code> flag for project name.</p>
<pre><code>cd {{ root }} <pre><code>cd {{ root }}
docker-compose up -d docker-compose -p mailu up -d
</pre></code> </pre></code>
Before you can use Mailu, you must create the primary administrator user account. This should be {{ postmaster }}@{{ domain }}. Use the following command, changing PASSWORD to your liking: Before you can use Mailu, you must create the primary administrator user account. This should be {{ postmaster }}@{{ domain }}. Use the following command, changing PASSWORD to your liking:
<pre><code>docker-compose exec admin python manage.py admin {{ postmaster }} {{ domain }} PASSWORD <pre><code>docker-compose -p mailu exec admin python manage.py admin {{ postmaster }} {{ domain }} PASSWORD
</pre></code> </pre></code>
<p>Login to the admin interface to change the password for a safe one, at <p>Login to the admin interface to change the password for a safe one, at

@ -10,14 +10,15 @@ services:
# External dependencies # External dependencies
redis: redis:
image: redis:alpine image: redis:alpine
restart: always
volumes: volumes:
- "{{ root }}/redis:/data" - "{{ root }}/redis:/data"
# Core services # Core services
front: front:
image: mailu/nginx:{{ version }} image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
logging:
driver: {{ log_driver or 'json-file' }}
ports: ports:
{% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %} {% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %}
- target: {{ port }} - target: {{ port }}
@ -28,7 +29,7 @@ services:
- "{{ root }}/certs:/certs" - "{{ root }}/certs:/certs"
- "{{ root }}/overrides/nginx:/overrides" - "{{ root }}/overrides/nginx:/overrides"
deploy: deploy:
replicas: 1 replicas: {{ front_replicas }}
{% if resolver_enabled %} {% if resolver_enabled %}
resolver: resolver:
@ -40,7 +41,7 @@ services:
{% endif %} {% endif %}
admin: admin:
image: mailu/admin:{{ version }} image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
{% if not admin_enabled %} {% if not admin_enabled %}
ports: ports:
@ -50,10 +51,10 @@ services:
- "{{ root }}/data:/data" - "{{ root }}/data:/data"
- "{{ root }}/dkim:/dkim" - "{{ root }}/dkim:/dkim"
deploy: deploy:
replicas: 1 replicas: {{ admin_replicas }}
imap: imap:
image: mailu/dovecot:{{ version }} image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
environment: environment:
# Default to 10.0.1.0/24 # Default to 10.0.1.0/24
@ -62,26 +63,24 @@ services:
- "{{ root }}/mail:/mail" - "{{ root }}/mail:/mail"
- "{{ root }}/overrides:/overrides" - "{{ root }}/overrides:/overrides"
deploy: deploy:
replicas: 1 replicas: {{ imap_replicas }}
smtp: smtp:
image: mailu/postfix:{{ version }} image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
environment: environment:
- POD_ADDRESS_RANGE={{ subnet }} - POD_ADDRESS_RANGE={{ subnet }}
volumes: volumes:
- "{{ root }}/overrides:/overrides" - "{{ root }}/overrides:/overrides"
deploy: deploy:
replicas: 1 replicas: {{ smtp_replicas }}
{% if resolver_enabled %} {% if resolver_enabled %}
dns: dns:
- {{ dns }} - {{ dns }}
{% endif %} {% endif %}
# Optional services
{% if antispam_enabled %}
antispam: antispam:
image: mailu/rspamd:{{ version }} image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
environment: environment:
- POD_ADDRESS_RANGE={{ subnet }} - POD_ADDRESS_RANGE={{ subnet }}
@ -95,11 +94,11 @@ services:
dns: dns:
- {{ dns }} - {{ dns }}
{% endif %} {% endif %}
{% endif %}
# Optional services
{% if antivirus_enabled %} {% if antivirus_enabled %}
antivirus: antivirus:
image: mailu/clamav:{{ version }} image: ${DOCKER_ORG:-mailu}/clamav:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/filter:/data" - "{{ root }}/filter:/data"
@ -113,7 +112,7 @@ services:
{% if webdav_enabled %} {% if webdav_enabled %}
webdav: webdav:
image: mailu/none:{{ version }} image: ${DOCKER_ORG:-mailu}/none:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/dav:/data" - "{{ root }}/dav:/data"
@ -123,7 +122,7 @@ services:
{% if fetchmail_enabled %} {% if fetchmail_enabled %}
fetchmail: fetchmail:
image: mailu/fetchmail:{{ version }} image: ${DOCKER_ORG:-mailu}/fetchmail:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/data:/data" - "{{ root }}/data:/data"
@ -137,7 +136,7 @@ services:
{% if webmail_type != 'none' %} {% if webmail_type != 'none' %}
webmail: webmail:
image: mailu/roundcube:{{ version }} image: ${DOCKER_ORG:-mailu}/roundcube:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }} env_file: {{ env }}
volumes: volumes:
- "{{ root }}/webmail:/data" - "{{ root }}/webmail:/data"

@ -0,0 +1,34 @@
$(document).ready(function() {
if ($("#webmail").val() == 'none') {
$("#webmail_path").hide();
$("#webmail_path").attr("value", "");
} else {
$("#webmail_path").show();
$("#webmail_path").attr("value", "/webmail");
}
$("#webmail").click(function() {
if (this.value == 'none') {
$("#webmail_path").hide();
$("#webmail_path").attr("value", "");
} else {
$("#webmail_path").show();
$("#webmail_path").attr("value", "/webmail");
}
});
});
$(document).ready(function() {
if ($('#admin').prop('checked')) {
$("#admin_path").show();
$("#admin_path").attr("value", "/admin");
}
$("#admin").change(function() {
if ($(this).is(":checked")) {
$("#admin_path").show();
$("#admin_path").attr("value", "/admin");
} else {
$("#admin_path").hide();
$("#admin_path").attr("value", "");
}
});
});

@ -15,15 +15,14 @@ accessing messages for beginner users.</p>
<!-- {{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }} --> <!-- {{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }} -->
<!-- </div> --> <!-- </div> -->
<br/> <br/>
<select class="btn btn-primary dropdown-toggle" name="webmail_type"> <select class="btn btn-primary dropdown-toggle" name="webmail_type" id="webmail">
{% for webmailtype in ["none", "roundcube", "rainloop"] %} {% for webmailtype in ["none", "roundcube", "rainloop"] %}
<option value="{{ webmailtype }}" >{{ webmailtype }}</option> <option value="{{ webmailtype }}" >{{ webmailtype }}</option>
{% endfor %} {% endfor %}
</select> </select>
<p></p> <p></p>
<div class="input-group"> <div class="input-group">
<!-- <div class="input-group-addon"><input type="checkbox" name="webmail_enabled" value="true"></div> --> <input class="form-control" type="text" name="webmail_path" id="webmail_path" style="display: none">
<input class="form-control" type="text" name="webmail_path" value="/webmail">
</div> </div>
</div> </div>
@ -32,12 +31,6 @@ will prevent Mailu from doing spam filtering, virus filtering, and from applying
white and blacklists that you may configure in the admin interface. You may white and blacklists that you may configure in the admin interface. You may
also disable the antivirus if required (it does use aroung 1GB of ram).</p> also disable the antivirus if required (it does use aroung 1GB of ram).</p>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="antispam_enabled" value="rspamd" checked>
Enable the spam filtering service
</label>
</div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> <label class="form-check-label">
<input class="form-check-input" type="checkbox" name="antivirus_enabled" value="clamav"> <input class="form-check-input" type="checkbox" name="antivirus_enabled" value="clamav">
@ -59,4 +52,9 @@ also disable the antivirus if required (it does use aroung 1GB of ram).</p>
</label> </label>
</div> </div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='render.js') }}"></script>
{% endcall %} {% endcall %}

@ -68,11 +68,13 @@ Or in plain english: if receivers start to classify your mail as spam, this post
manage your email domains, users, etc.</p> manage your email domains, users, etc.</p>
<div class="form-group"> <div class="form-group">
<label>Enable the admin UI (and path to the admin UI)</label> <input type="checkbox" name="admin_enabled" value="true" id="admin">
<div class="input-group"> <label>Enable the admin UI (and path to the admin UI)</label>
<div class="input-group-addon"><input type="checkbox" name="admin_enabled" value="true"></div> <input class="form-control" type="text" name="admin_path" id="admin_path" style="display: none">
<input class="form-control" type="text" name="admin_path" value="/admin">
</div>
</div> </div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='render.js') }}"></script>
{% endcall %} {% endcall %}

@ -15,15 +15,14 @@ accessing messages for beginner users.</p>
<!-- {{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }} --> <!-- {{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }} -->
<!-- </div> --> <!-- </div> -->
<br/> <br/>
<select class="btn btn-primary dropdown-toggle" name="webmail_type"> <select class="btn btn-primary dropdown-toggle" name="webmail_type" id="webmail">
{% for webmailtype in ["none", "roundcube", "rainloop"] %} {% for webmailtype in ["none", "roundcube", "rainloop"] %}
<option value="{{ webmailtype }}" >{{ webmailtype }}</option> <option value="{{ webmailtype }}" >{{ webmailtype }}</option>
{% endfor %} {% endfor %}
</select> </select>
<p></p> <p></p>
<div class="input-group"> <div class="input-group">
<!-- <div class="input-group-addon"><input type="checkbox" name="webmail_enabled" value="true"></div> --> <input class="form-control" type="text" name="webmail_path" id="webmail_path" style="display: none">
<input class="form-control" type="text" name="webmail_path" value="/webmail">
</div> </div>
</div> </div>
@ -32,12 +31,6 @@ will prevent Mailu from doing spam filtering, virus filtering, and from applying
white and blacklists that you may configure in the admin interface. You may white and blacklists that you may configure in the admin interface. You may
also disable the antivirus if required (it does use aroung 1GB of ram).</p> also disable the antivirus if required (it does use aroung 1GB of ram).</p>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="antispam_enabled" value="rspamd" checked>
Enable the spam filtering service
</label>
</div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> <label class="form-check-label">
<input class="form-check-input" type="checkbox" name="antivirus_enabled" value="clamav"> <input class="form-check-input" type="checkbox" name="antivirus_enabled" value="clamav">
@ -59,4 +52,8 @@ also disable the antivirus if required (it does use aroung 1GB of ram).</p>
</label> </label>
</div> </div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='render.js') }}"></script>
{% endcall %} {% endcall %}

@ -0,0 +1,28 @@
{% call macros.panel("info", "Step 5 - Number of replicas for containers") %}
<p>Select number of replicas for containers</p>
<div class="form-group">
<input class="form-control" type="number" name="front_replicas" min="1" required value="1"
style="width: 6%; display: inline;">
<label>Front</label>
</div>
<div class="form-group">
<input class="form-control" type="number" name="admin_replicas" min="1" required value="1"
style="width: 6%; display: inline;">
<label>Admin</label>
</div>
<div class="form-group">
<input class="form-control" type="number" name="imap_replicas" min="1" required value="1"
style="width: 6%; display: inline;">
<label>IMAP</label>
</div>
<div class="form-group">
<input class="form-control" type="number" name=smtp_replicas min="1" required value="1"
style="width: 6%; display: inline;">
<label>SMPT</label>
</div>
{% endcall %}

@ -3,58 +3,58 @@ version: '3'
services: services:
front: front:
image: ${DOCKER_ORG:-mailu}/nginx:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}nginx:${MAILU_VERSION:-local}
build: ../core/nginx build: ../core/nginx
resolver: resolver:
image: ${DOCKER_ORG:-mailu}/unbound:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}unbound:${MAILU_VERSION:-local}
build: ../services/unbound build: ../services/unbound
imap: imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}dovecot:${MAILU_VERSION:-local}
build: ../core/dovecot build: ../core/dovecot
smtp: smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}postfix:${MAILU_VERSION:-local}
build: ../core/postfix build: ../core/postfix
antispam: antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}rspamd:${MAILU_VERSION:-local}
build: ../services/rspamd build: ../services/rspamd
antivirus: antivirus:
image: ${DOCKER_ORG:-mailu}/clamav:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}clamav:${MAILU_VERSION:-local}
build: ../optional/clamav build: ../optional/clamav
webdav: webdav:
image: ${DOCKER_ORG:-mailu}/radicale:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}radicale:${MAILU_VERSION:-local}
build: ../optional/radicale build: ../optional/radicale
admin: admin:
image: ${DOCKER_ORG:-mailu}/admin:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}admin:${MAILU_VERSION:-local}
build: ../core/admin build: ../core/admin
roundcube: roundcube:
image: ${DOCKER_ORG:-mailu}/roundcube:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}roundcube:${MAILU_VERSION:-local}
build: ../webmails/roundcube build: ../webmails/roundcube
rainloop: rainloop:
image: ${DOCKER_ORG:-mailu}/rainloop:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}rainloop:${MAILU_VERSION:-local}
build: ../webmails/rainloop build: ../webmails/rainloop
fetchmail: fetchmail:
image: ${DOCKER_ORG:-mailu}/fetchmail:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}fetchmail:${MAILU_VERSION:-local}
build: ../services/fetchmail build: ../services/fetchmail
none: none:
image: ${DOCKER_ORG:-mailu}/none:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}none:${MAILU_VERSION:-local}
build: ../core/none build: ../core/none
docs: docs:
image: ${DOCKER_ORG:-mailu}/docs:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}docs:${MAILU_VERSION:-local}
build: ../docs build: ../docs
setup: setup:
image: ${DOCKER_ORG:-mailu}/setup:${VERSION:-local} image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}setup:${MAILU_VERSION:-local}
build: ../setup build: ../setup

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE/jCCAuagAwIBAgIJAKVnyadXS7SuMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAeFw0xODEwMzExMDE1MzFaFw0yODEwMjgxMDE1MzFaMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBAOQ2ZDqR+YvW5FKykBXz/Ec+jSb0Lv7GYQkT5t+TB1NXuR+QH1LfNWFmXOo7
YXcPVXlmcuLDuUldrctdS59fx8dnFu5gRRUqJwZuEQICypsX0rTDtsV6xqZB8c8y
2+BztP9OHfPpZdnU1IBx2fDbjpdKUaoAMFMFvyTaEcIyp6aGAhejvJCwc3D8fIJI
NhWA2O11sZQHUs7/MHzpu/IHpgutgk8EsNOUNLwB3+9p3IlOlTT6GilIXOYeTzoD
hiI6B5BQqXHsRrkao3v0YL6Ekun4hOx3MYx09AZtmuyrlq1mkNueKS5JwKDrXXbq
Ta0oyJ18UTZFRwVqApcuR4CA8vuhI9PsoDCvBQH1rW6FyiM4bhybatFJAYjQAODe
gwh2p6JWux5C1gaBUubOrKO7o5ePI6s0MmK8ZxrL4PpBYt3B33ztFfjWmVbCTSvP
GuQ2Ux73OY2NNxx2aNt4Th0IxrvMdsGLrZsdma2rWa5eTJTAuqbSjI/Wb1zjO0pi
pwoxk6f1COFLopo2xgJj6+KKG1nKLfOzQFexcpdq/mpuulcVcLDPJzJTLX3qsgtD
iBpm1ozNRT+M7XUavg8aHNfn6S+TcDb5hp+1yZ6obZq/VlA6atk0fuPzf+ndQ0fq
YN1jlAIzZXt/Dpc+ObjS09WGDVQXobGesdwA6BH14OV+TxOHAgMBAAGjUzBRMB0G
A1UdDgQWBBQy7kA8FbdcFpVU1AoFgzE7Fw1QqDAfBgNVHSMEGDAWgBQy7kA8Fbdc
FpVU1AoFgzE7Fw1QqDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IC
AQBLFlQKxztxm7MtsHs01Pl8/FpKzWekWK1ksf15d8mHBT30OTs+NXaJDuHTGL4r
rPeFf3NZ1PZkGRnJCEWur+8e8Y5KwuMAaagneSYXU0gcZfvTidvf865Jiml8xO5x
PAo8qTZQCHmYcvJQwBXMkq/2sFJCYeMOLoJdXXbTTe2ZQ/N3MSQbpgWJ8pF7srKU
biw2RkNH39QPq9GpWRQGx2gwvZDy2oFG8cM1hJYmz0Y9clpBE0mSqypvA1E8ufKC
uaUc0tpPI5H4efeWv/ObnFAJ3DMEmzUnQ8hdM/7cpf6AL8VRm4Wrw112gK7SbSdd
mMsUfFIDfyE9vsZ3OC8C8LqXKLwMcm7Fdq0ym0NINtoVW0ukmVJzB78CdWaJ7ux1
WqitcnewgiMWuuwuepBmNurZtgDrg+zgMhNpuK0NzYyE+ZReoJIOJCub3SSEsWdl
x5aJEYuFYJR5EvmxWeYv5p1GVOTL1TJqW7iRodzRoMc9u2vb0+tCbM5XSZVPul6P
QimDui2Ogq0zYNbSkHaUGBpjGDvHYG0zXO2sWrdrAJQMHo8dGEe7FuSuAlWbQdb/
xgN4uwejxV6B2e6rjT6YMni+r5Qw0EhNka+Xohw5E68bEcQSrCP8j64qQLAeipuz
ImqBTNyyR4WTcL+1HIVM7ZIw3igHH55zo5qTvyjKyZX9Uw==
-----END CERTIFICATE-----

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDkNmQ6kfmL1uRS
spAV8/xHPo0m9C7+xmEJE+bfkwdTV7kfkB9S3zVhZlzqO2F3D1V5ZnLiw7lJXa3L
XUufX8fHZxbuYEUVKicGbhECAsqbF9K0w7bFesamQfHPMtvgc7T/Th3z6WXZ1NSA
cdnw246XSlGqADBTBb8k2hHCMqemhgIXo7yQsHNw/HyCSDYVgNjtdbGUB1LO/zB8
6bvyB6YLrYJPBLDTlDS8Ad/vadyJTpU0+hopSFzmHk86A4YiOgeQUKlx7Ea5GqN7
9GC+hJLp+ITsdzGMdPQGbZrsq5atZpDbnikuScCg61126k2tKMidfFE2RUcFagKX
LkeAgPL7oSPT7KAwrwUB9a1uhcojOG4cm2rRSQGI0ADg3oMIdqeiVrseQtYGgVLm
zqyju6OXjyOrNDJivGcay+D6QWLdwd987RX41plWwk0rzxrkNlMe9zmNjTccdmjb
eE4dCMa7zHbBi62bHZmtq1muXkyUwLqm0oyP1m9c4ztKYqcKMZOn9QjhS6KaNsYC
Y+viihtZyi3zs0BXsXKXav5qbrpXFXCwzycyUy196rILQ4gaZtaMzUU/jO11Gr4P
GhzX5+kvk3A2+YaftcmeqG2av1ZQOmrZNH7j83/p3UNH6mDdY5QCM2V7fw6XPjm4
0tPVhg1UF6GxnrHcAOgR9eDlfk8ThwIDAQABAoICACoHsnHvDIyqqSZp6IuCggYF
CS4Rbs5RbvGjDrRCeejpkRi1DG/Q2B32IkqpYQvycQWIzsPg1DEk5as8pX7Wvw6E
d/6zEEYTm1hd0RgTt4jU3GOaYAEC2a8pGgXVEhXGeaFDm9SeObnirrhxP3hSl3JZ
p6ytmDjSKB/7YaXoemP67ku4RjRHqxs2BSBheESBlHI3aNsgdinVafK3gXvT2Mrx
y7wN2xs8gnHVzo5jatCG/ofhQAw2XZWsI19F4uBO27HCiVKH94aD13Quz9qGxB//
O0vpr+B0cbT1XsET4Q5Sg39PI7p4rtd0QaRzBpdLmZcXnEVogOoIWi3JwjVyik1g
lcg+4A8wj4pDGsCmANt90YqedktQGiYsYozZHO3YCrnjO6lqYJLOBocRG9NJqldY
kzs6UfJ+96FoYQVGNXyeQZizC26rQHll/rwsJnsB7GvM38f3q3cr3Borpwx3HosN
mmM+WRcvV3WWjjx1870Jm+tIDu0clWvT7hdHSf4938/Xr9cUTyuX2LrqTfp6JThl
+NbYgbuvd5leP94wPwRxfJL+PR5B4kbLPwDNCbpM8QTBm+9Y4kU+6ePmgcuRemMQ
8J41ocUjC4wR2j9Zgy0f0Rz4KiKM6IiVgKyqPUMaY+aJQ+yB5J+tlBkPJeZzft/e
XAoxt0STTassHC+p9COxAoIBAQD2Vd2Q1rbxWGnWl0m1LcH5q4hsuUAhIYmuTMSO
RkDLD/8yfPR4uUbTgrtdL2FaeOsCK7nrQAPxcfdD//+SoNVsAkMuNw6QvJn4ZXLf
5C45tN4pfoz/EwIRBvyJnI+HZuNaCUCfsQB9ggeEHgM2n36GBiOX82inQey3eREz
wZjQqmCp+b1QiYoWrVCgOPOvB86kbNgHGacIS7cDe94OeP4dH+FAfWaIBab8sDnG
K6+N6dWdj+b7veUWpXBs8beVCTO4GPnW5hnYOfuWkdpNCej/QbMeivMA4U7g+CeF
Y5QB07EE5f35Epp8WoNtwVZoFgP72xMT1taz1Rx7dohdYvLVAoIBAQDtKoDiwi2V
07rOgsjgW972HdA0nOnja/lky6CKkY5BqNGMj63h0ysy8Fe8mEWdPXyY9f7TgWP9
sDMZMq+d8ZwAjfdYjYTKpxA3pA9oj66OCxtR6usElmeyultPjZ8FXJNXzOLv4dju
FnELSFSSx8o6WHGq9l2eWNMFf46g70Bt+aiHV/VGLLSFTUcvd51H7jP+PFxrBn1k
kz1u0n/RRuPMIru68lKJxrpDsr917Spw16O+uzjR99IqNPskVJxUnXV8qvMxeWVl
wTOP9soqYv/KvqjsBO+nLNkLSH402Fp78e2Oe6KPKlF21kl5oA7Yn/w4MtyFpj65
fg6uDaPhgoLrAoIBAQCb9uWfzLJrwETSn1sFoYENKPPpkqjt0SQw/V39jrF7YBd9
yeune/dB96XVbChBdgmliDXgotlcR4H8xdr05Wv7RLtwSV+peCAsS18eLoSt+Lwo
nX18CnbmfPvrzPp7CkOsP+twsErVLDzCA5aZQQaEqOJkVLLQI0dTKw4fLNYqV5V4
SSz6DvslPHqt1yFCkrjdFiT46d79u6KWTBjeJPEPU530jPEb8ig2GQWbWRF/0qtz
ZSckAKlJW1oBQFGxxO/AAeA9ldaLNrr6LEKBQGMLKnfUQLl2tzCP885iABg3x+Zu
aYgR6Rty3IQWO7EPmdDP53b+uqmZlra/3N6d8gY5AoIBADxkBk23hEQSlg7f3qbC
vhONo+bBzgzLAcZY05h1V/QAONvB+lT2oJln+e9cFt3jOkb43NqeqAeBRoG0FmPx
kffSLpmt75Jq2AZTEFlfvOMOkPZbC10vr1gje/zV4xhKanqBAYhzyflWXZKx6Fc3
6JbSzp7p/QzFMXbE9Fymj5FxcSiFjT9BQvZupyG/I52dWj/yvtXB4Uwq8gm2MDXq
BzeD4KnJ6pqKsANtELPGoHf7cQawRdexcyKsOwcVRHmHXtNP9H00nE081RRjkzcX
3mqSAhGXcC7xjJMC8qAiN2g4QnV1pf8ul2/bQPpnd2BR3Leyu9SMcIxrPPG1J3XU
9eECggEBAMMhMURUfLSXIkreMfxH4rSqk0r2xQ1rE1ChAIBQPfyx4KWUkBTdpoiv
uKcPzAgN+bm3Y5wRGwoE22Ac0lWobnzaIYyYN9N7HU+86q92ozWW1lCUEE0kBt2r
FnWCD/3B0LOX2Cn8HHYzroRmzMlRvBa7/GO1dqURz/OzjTWN0+k9mgE7oS5M8fQV
AS3mxXZMPKSB0xTfJoXW8ui9MQZHcNSkNORNP/2doCkR2qDUkazbhi/3ghLmDGVJ
p5OrIPQUwcp1bFOciX22fAaZwoa63ng3K+WZjSqqma05AiOc59MhDLAu6a0rKKO1
W3079UVfBB4hkfN2721fqyj+r/0z+R0=
-----END PRIVATE KEY-----

@ -0,0 +1,4 @@
echo "Creating users ..."
docker-compose -f tests/compose/core/docker-compose.yml exec admin flask mailu admin admin mailu.io password || exit 1
docker-compose -f tests/compose/core/docker-compose.yml exec admin flask mailu user user mailu.io 'password' 'SHA512-CRYPT' || exit 1
echo "Admin and user successfully created!"

@ -0,0 +1 @@
python3 tests/email_test.py message-core

@ -0,0 +1,80 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
# Webmail

@ -1,31 +1,35 @@
# Mailu main configuration file # Mailu main configuration file
# #
# Most configuration variables can be modified through the Web interface, # Generated for compose flavor
# these few settings must however be configured before starting the mail #
# server and require a restart upon change. # This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
################################### ###################################
# Common configuration variables # Common configuration variables
################################### ###################################
# Set this to the path where Mailu data and configuration is stored # Set this to the path where Mailu data and configuration is stored
ROOT=/mailu # This variable is now set directly in `docker-compose.yml by the setup utility
# ROOT=/mailu
# Mailu version to run (1.0, 1.1, etc. or master) # Mailu version to run (1.0, 1.1, etc. or master)
#VERSION=master #VERSION=master
# Set to a randomly generated 16 bytes string # Set to a randomly generated 16 bytes string
SECRET_KEY=ChangeMeChangeMe SECRET_KEY=HGZCYGVI6FVG31HS
# Address where listening ports should bind # Address where listening ports should bind
BIND_ADDRESS4=127.0.0.1 # This variables are now set directly in `docker-compose.yml by the setup utility
#BIND_ADDRESS6=::1 # PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Main mail domain # Main mail domain
DOMAIN=mailu.io DOMAIN=mailu.io
# Hostnames for this server, separated with comas # Hostnames for this server, separated with comas
HOSTNAMES=mail.mailu.io,alternative.mailu.io,yetanother.mailu.io HOSTNAMES=localhost
# Postmaster local part (will append the main mail domain) # Postmaster local part (will append the main mail domain)
POSTMASTER=admin POSTMASTER=admin
@ -34,7 +38,7 @@ POSTMASTER=admin
TLS_FLAVOR=cert TLS_FLAVOR=cert
# Authentication rate limit (per source IP address) # Authentication rate limit (per source IP address)
AUTH_RATELIMIT=10/minute;1000/hour AUTH_RATELIMIT=10/minute;1000/hour
# Opt-out of statistics, replace with "True" to opt out # Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS=False DISABLE_STATISTICS=False
@ -44,7 +48,7 @@ DISABLE_STATISTICS=False
################################### ###################################
# Expose the admin interface (value: true, false) # Expose the admin interface (value: true, false)
ADMIN=false ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none) # Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=none WEBMAIL=none
@ -53,7 +57,10 @@ WEBMAIL=none
WEBDAV=none WEBDAV=none
# Antivirus solution (value: clamav, none) # Antivirus solution (value: clamav, none)
ANTIVIRUS=none #ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
################################### ###################################
# Mail settings # Mail settings
@ -65,7 +72,7 @@ MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker # Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16) # internal network (default to 172.17.0.0/16)
RELAYNETS=172.16.0.0/12 RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured # Will relay all outgoing mails if configured
RELAYHOST= RELAYHOST=
@ -74,18 +81,12 @@ RELAYHOST=
FETCHMAIL_DELAY=600 FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part # Recipient delimiter, character used to delimiter localpart from custom address part
# e.g. localpart+custom@domain;tld
RECIPIENT_DELIMITER=+ RECIPIENT_DELIMITER=+
# DMARC rua and ruf email # DMARC rua and ruf email
DMARC_RUA=admin DMARC_RUA=admin
DMARC_RUF=admin DMARC_RUF=admin
# 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!
# Maildir Compression # Maildir Compression
# choose compression-method, default: none (value: bz2, gz) # choose compression-method, default: none (value: bz2, gz)
@ -109,12 +110,7 @@ SITENAME=Mailu
# Linked Website URL # Linked Website URL
WEBSITE=https://mailu.io WEBSITE=https://mailu.io
# 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 # Advanced settings
@ -124,17 +120,20 @@ WEBSITE=https://mailu.io
# json-file (default) # json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration) # journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!) # syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
LOG_DRIVER=json-file # LOG_DRIVER=json-file
# Docker-compose project name, this will prepended to containers names. # Docker-compose project name, this will prepended to containers names.
#COMPOSE_PROJECT_NAME=mailu COMPOSE_PROJECT_NAME=mailu
# Default password scheme used for newly created accounts and changed passwords # Default password scheme used for newly created accounts and changed passwords
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT) # (value: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=SHA512-CRYPT PASSWORD_SCHEME=BLF-CRYPT
# Header to take the real ip from # Header to take the real ip from
REAL_IP_HEADER= REAL_IP_HEADER=
# IPs for nginx set_real_ip_from (CIDR list separated by commas) # IPs for nginx set_real_ip_from (CIDR list separated by commas)
REAL_IP_FROM= REAL_IP_FROM=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -0,0 +1,84 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
fetchmail:
image: ${DOCKER_ORG:-mailu}/fetchmail:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
# Webmail

@ -0,0 +1,139 @@
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# 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=JS48Q9KE3B6T97E6
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Main mail domain
DOMAIN=mailu.io
# Hostnames for this server, separated with comas
HOSTNAMES=localhost
# 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
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=none
# Dav server implementation (value: radicale, none)
WEBDAV=none
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# 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: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-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=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -0,0 +1,6 @@
python3 tests/email_test.py message-virus "tests/compose/filters/eicar.com"
if [ $? -eq 99 ]; then
exit 0
else
exit 1
fi

@ -0,0 +1,86 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
antivirus:
image: ${DOCKER_ORG:-mailu}/clamav:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/data"
# Webmail

@ -0,0 +1 @@
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

@ -0,0 +1,139 @@
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# 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=11H6XURLGE7GW3U1
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Main mail domain
DOMAIN=mailu.io
# Hostnames for this server, separated with comas
HOSTNAMES=localhost
# 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
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=none
# Dav server implementation (value: radicale, none)
WEBDAV=none
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=clamav
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# 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: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-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=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -0,0 +1,88 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
# Webmail
webmail:
image: ${DOCKER_ORG:-mailu}/rainloop:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/webmail:/data"
depends_on:
- imap

@ -0,0 +1,139 @@
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# 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=V5J4SHRYVW9PZIQU
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Main mail domain
DOMAIN=mailu.io
# Hostnames for this server, separated with comas
HOSTNAMES=localhost
# 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
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=rainloop
# Dav server implementation (value: radicale, none)
WEBDAV=none
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# 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: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-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=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -0,0 +1,88 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
# Webmail
webmail:
image: ${DOCKER_ORG:-mailu}/roundcube:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/webmail:/data"
depends_on:
- imap

@ -0,0 +1,139 @@
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# 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=PGGO2JRQ59QV3DW7
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Main mail domain
DOMAIN=mailu.io
# Hostnames for this server, separated with comas
HOSTNAMES=localhost
# 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
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=roundcube
# Dav server implementation (value: radicale, none)
WEBDAV=none
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# 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: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-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=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -1,101 +0,0 @@
version: '2'
services:
front:
image: $DOCKER_ORG/nginx:$VERSION
restart: 'no'
env_file: $PWD/.env
logging:
driver: $LOG_DRIVER
ports:
- "$BIND_ADDRESS4:80:80"
- "$BIND_ADDRESS4:443:443"
- "$BIND_ADDRESS4:110:110"
- "$BIND_ADDRESS4:143:143"
- "$BIND_ADDRESS4:993:993"
- "$BIND_ADDRESS4:995:995"
- "$BIND_ADDRESS4:25:25"
- "$BIND_ADDRESS4:465:465"
- "$BIND_ADDRESS4:587:587"
volumes:
- "$ROOT/certs:/certs"
redis:
image: redis:alpine
restart: 'no'
volumes:
- "$ROOT/redis:/data"
imap:
image: $DOCKER_ORG/dovecot:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/data:/data"
- "$ROOT/mail:/mail"
- "$ROOT/overrides:/overrides"
depends_on:
- front
smtp:
image: $DOCKER_ORG/postfix:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/data:/data"
- "$ROOT/overrides:/overrides"
depends_on:
- front
antispam:
image: $DOCKER_ORG/rspamd:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/filter:/var/lib/rspamd"
- "$ROOT/dkim:/dkim"
- "$ROOT/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
antivirus:
image: $DOCKER_ORG/$ANTIVIRUS:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/filter:/data"
webdav:
image: $DOCKER_ORG/$WEBDAV:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/dav:/data"
admin:
image: $DOCKER_ORG/admin:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/data:/data"
- "$ROOT/dkim:/dkim"
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- redis
webmail:
image: "$DOCKER_ORG/$WEBMAIL:$VERSION"
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/webmail:/data"
depends_on:
- imap
fetchmail:
image: $DOCKER_ORG/fetchmail:$VERSION
restart: 'no'
env_file: $PWD/.env
volumes:
- "$ROOT/data:/data"

@ -1,57 +0,0 @@
#!/bin/bash
containers=(
webmail
imap
smtp
antispam
admin
redis
antivirus
webdav
# fetchmail
front
)
# Time to sleep in minutes after starting the containers
WAIT=1
containers_check() {
status=0
for container in "${containers[@]}"; do
name="${DOCKER_ORG}_${container}_1"
echo "Checking $name"
docker inspect "$name" | grep '"Status": "running"' || status=1
done
docker ps -a
return $status
}
container_logs() {
for container in "${containers[@]}"; do
name="${DOCKER_ORG}_${container}_1"
echo "Showing logs for $name"
docker container logs "$name"
done
}
clean() {
docker-compose -f tests/compose/run.yml -p $DOCKER_ORG down || exit 1
rm -fv .env
}
# Cleanup before callig exit
die() {
clean
exit $1
}
for file in tests/compose/*.env ; do
cp $file .env
docker-compose -f tests/compose/run.yml -p $DOCKER_ORG up -d
echo -e "\nSleeping for ${WAIT} minutes" # Clean terminal distortion from docker-compose in travis
travis_wait sleep ${WAIT}m || sleep ${WAIT}m #Fallback sleep for local run
container_logs
containers_check || die 1
clean
done

@ -0,0 +1,100 @@
import sys
import os
import time
import docker
from colorama import Fore, Style
import subprocess
# Declare variables for service name and sleep time
test_name=sys.argv[1]
timeout=int(sys.argv[2])
test_path="tests/compose/" + test_name + "/"
compose_file=test_path + "docker-compose.yml"
client = docker.APIClient(base_url='unix://var/run/docker.sock')
containers = []
# Stop containers
def stop(exit_code):
print_logs()
sys.stdout.flush()
print(subprocess.check_output("docker-compose -f " + compose_file + " down", shell=True).decode())
sys.exit(exit_code)
# Sleep for a defined amount of time
def sleep():
print(Fore.LIGHTMAGENTA_EX + "Sleeping for " + str(timeout) + "m" + Style.RESET_ALL)
time.sleep(timeout*60)
def health_checks():
exit_code = 0
#Iterating trough all containers dictionary
for container in client.containers(all=True):
#Perform "docker container inspect" on container based on container ID and save output to a dictionary
container_inspect = client.inspect_container(container['Id']) #Dict
if "Health" in container_inspect['State'].keys():
if container_inspect['State']['Health']['Status'] == "healthy":
print(Fore.GREEN + "Health status for " + container_inspect['Name'].replace("/", "") + " : " + Fore.CYAN + container_inspect['State']['Health']['Status'] + Style.RESET_ALL)
if container_inspect['State']['Health']['Status'] != "healthy":
print(Fore.RED + "Container " + container_inspect['Name'].replace("/", "") + " is " + Fore.YELLOW + container_inspect['State']['Health']['Status']
+ Fore.RED + ", FailingStreak: " + Fore.YELLOW + str(container_inspect['State']['Health']['FailingStreak'])
+ Fore.RED + ", Log: " + Fore.YELLOW + str(container_inspect['State']['Health']['Log']) + Style.RESET_ALL)
exit_code = 1
else:
if container_inspect['State']['Status'] == "running":
print(Fore.GREEN + "Running status for " + container_inspect['Name'].replace("/", "") + " : " + Fore.BLUE + container_inspect['State']['Status'] + Style.RESET_ALL)
if container_inspect['State']['Status'] != "running":
print(Fore.RED + "Container " + container_inspect['Name'].replace("/", "") + " state is: " + Fore.YELLOW + container_inspect['State']['Status'] + Style.RESET_ALL)
exit_code = 1
#Saving Id, Name and state to a new dictionary
containers_dict = {}
containers_dict['Name'] = container_inspect['Name'].replace("/", "")
containers_dict['Id'] = container_inspect['Id']
containers_dict['State'] = container_inspect['State']
#Adding the generated dictionary to a list
containers.append(containers_dict)
if exit_code != 0:
stop(exit_code)
def print_logs():
print("Printing logs ...")
#Iterating through docker container inspect list and print logs
for container in containers:
print(Fore.LIGHTMAGENTA_EX + "Printing logs for: " + Fore.GREEN + container['Name'] + Style.RESET_ALL)
sys.stdout.flush()
print(subprocess.check_output('docker container logs ' + container['Name'], shell=True).decode())
#Iterating over hooks in test folder and running them
def hooks():
print(Fore.LIGHTMAGENTA_EX + "Running hooks" + Style.RESET_ALL)
for test_file in sorted(os.listdir(test_path)):
try:
if test_file.endswith(".py"):
sys.stdout.flush()
print(subprocess.check_output("python3 " + test_path + test_file, shell=True).decode())
elif test_file.endswith(".sh"):
sys.stdout.flush()
print(subprocess.check_output("./" + test_path + test_file, shell=True).decode())
except subprocess.CalledProcessError as e:
sys.stderr.write("[ERROR]: output = %s, error code = %s\n" % (e.output.decode(), e.returncode))
stop(1)
# Start up containers
sys.stdout.flush()
print(subprocess.check_output("docker-compose -f " + compose_file + " up -d", shell=True).decode())
print()
sleep()
print()
sys.stdout.flush()
print(subprocess.check_output("docker ps -a", shell=True).decode())
print()
health_checks()
print()
hooks()
print()
stop(0)

@ -0,0 +1,86 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/nginx:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/admin:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
webdav:
image: ${DOCKER_ORG:-mailu}/radicale:${MAILU_VERSION:-master}
restart: always
env_file: mailu.env
volumes:
- "/mailu/dav:/data"
# Webmail

@ -0,0 +1,139 @@
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# 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=XVDDSWOAGVF5J9QJ
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Main mail domain
DOMAIN=mailu.io
# Hostnames for this server, separated with comas
HOSTNAMES=localhost
# 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
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=none
# Dav server implementation (value: radicale, none)
WEBDAV=radicale
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# 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: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-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=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -0,0 +1,59 @@
import smtplib
import imaplib
import time
import sys
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import ntpath
from email.mime.base import MIMEBase
from email import encoders
msg = MIMEMultipart()
msg['From'] = "admin@mailu.io"
msg['To'] = "user@mailu.io"
msg['Subject'] = "File Test"
msg.attach(MIMEText(sys.argv[1], 'plain'))
if len(sys.argv) == 3:
part = MIMEBase('application', 'octet-stream')
part.set_payload((open(sys.argv[2], "rb")).read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', "attachment; filename=%s" % ntpath.basename(sys.argv[2]))
msg.attach(part)
try:
smtp_server = smtplib.SMTP('localhost')
smtp_server.set_debuglevel(1)
smtp_server.connect('localhost', 587)
smtp_server.ehlo()
smtp_server.starttls()
smtp_server.ehlo()
smtp_server.login("admin@mailu.io", "password")
smtp_server.sendmail("admin@mailu.io", "user@mailu.io", msg.as_string())
smtp_server.quit()
except:
sys.exit(25)
time.sleep(30)
try:
imap_server = imaplib.IMAP4_SSL('localhost')
imap_server.login('user@mailu.io', 'password')
except:
sys.exit(110)
stat, count = imap_server.select('inbox')
try:
stat, data = imap_server.fetch(count[0], '(UID BODY[TEXT])')
except :
sys.exit(99)
if sys.argv[1] in str(data[0][1]):
print("Success sending and receiving email!")
else:
print("Failed receiving email with message %s" % sys.argv[1])
sys.exit(99)
imap_server.close()
imap_server.logout()

@ -0,0 +1,2 @@
docker
colorama

@ -22,7 +22,7 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists && rm -rf /var/lib/apt/lists
COPY include.php /var/www/html/include.php COPY include.php /var/www/html/include.php
COPY php.ini /usr/local/etc/php/conf.d/rainloop.ini COPY php.ini /php.ini
COPY config.ini /config.ini COPY config.ini /config.ini
COPY default.ini /default.ini COPY default.ini /default.ini

@ -1,7 +1,7 @@
; RainLoop Webmail configuration file ; RainLoop Webmail configuration file
[webmail] [webmail]
attachment_size_limit = 25 attachment_size_limit = {{ MAX_FILESIZE }}
[security] [security]
allow_admin_panel = Off allow_admin_panel = Off

@ -1,3 +1,4 @@
date.timezone=UTC date.timezone=UTC
upload_max_filesize = 25M upload_max_filesize = {{ MAX_FILESIZE }}M
post_max_size = 25M post_max_size = {{ MAX_FILESIZE }}M

@ -10,6 +10,8 @@ convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()
os.environ["FRONT_ADDRESS"] = os.environ.get("FRONT_ADDRESS", "front") os.environ["FRONT_ADDRESS"] = os.environ.get("FRONT_ADDRESS", "front")
os.environ["IMAP_ADDRESS"] = os.environ.get("IMAP_ADDRESS", "imap") os.environ["IMAP_ADDRESS"] = os.environ.get("IMAP_ADDRESS", "imap")
os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))
base = "/data/_data_/_default_/" base = "/data/_data_/_default_/"
shutil.rmtree(base + "domains/", ignore_errors=True) shutil.rmtree(base + "domains/", ignore_errors=True)
os.makedirs(base + "domains", exist_ok=True) os.makedirs(base + "domains", exist_ok=True)
@ -17,6 +19,7 @@ os.makedirs(base + "configs", exist_ok=True)
convert("/default.ini", "/data/_data_/_default_/domains/default.ini") convert("/default.ini", "/data/_data_/_default_/domains/default.ini")
convert("/config.ini", "/data/_data_/_default_/configs/config.ini") convert("/config.ini", "/data/_data_/_default_/configs/config.ini")
convert("/php.ini", "/usr/local/etc/php/conf.d/rainloop.ini")
os.system("chown -R www-data:www-data /data") os.system("chown -R www-data:www-data /data")

@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y \
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.8/roundcubemail-1.3.8-complete.tar.gz ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.8/roundcubemail-1.3.8-complete.tar.gz
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
zlib1g-dev \ zlib1g-dev python3-jinja2 \
&& docker-php-ext-install zip \ && docker-php-ext-install zip \
&& echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini \ && echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini \
&& rm -rf /var/www/html/ \ && rm -rf /var/www/html/ \
@ -22,7 +22,7 @@ RUN apt-get update && apt-get install -y \
&& chown -R www-data: logs temp \ && chown -R www-data: logs temp \
&& rm -rf /var/lib/apt/lists && rm -rf /var/lib/apt/lists
COPY php.ini /usr/local/etc/php/conf.d/roundcube.ini COPY php.ini /php.ini
COPY config.inc.php /var/www/html/config/ COPY config.inc.php /var/www/html/config/
COPY start.py /start.py COPY start.py /start.py

@ -1,3 +1,4 @@
date.timezone=UTC date.timezone=UTC
upload_max_filesize = 25M upload_max_filesize = {{ MAX_FILESIZE }}M
post_max_size = 25M post_max_size = {{ MAX_FILESIZE }}M

@ -1,6 +1,13 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import jinja2
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))
convert("/php.ini", "/usr/local/etc/php/conf.d/roundcube.ini")
# Fix some permissions # Fix some permissions
os.system("mkdir -p /data/gpg") os.system("mkdir -p /data/gpg")

Loading…
Cancel
Save