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:
default: null
branches:
master:
protection:
required_status_checks:
contexts:
- continuous-integration/travis-ci
required_pull_request_reviews:
required_approving_review_count: 2
pull_request_rules:
- name: Successful travis and 2 approved reviews
conditions:
- status-success=continuous-integration/travis-ci/pr
- label!=["status"/wip","status/blocked"]
- "#approved-reviews-by>=2"
actions:
merge:
method: merge
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:
packages:
- docker-ce
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:
# Default to mailu for DOCKER_ORG
- if [ -z "$DOCKER_ORG" ]; then export DOCKER_ORG="mailu"; fi
- docker-compose -f tests/build.yml build
- tests/compose/test-script.sh
# test.py, test name and timeout between start and tests.
- python tests/compose/test.py core 1
- python tests/compose/test.py fetchmail 1
- 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:
provider: script
@ -19,4 +35,3 @@ deploy:
on:
all_branches: true
condition: -n $DOCKER_UN

@ -15,14 +15,14 @@ RUN apk add --no-cache openssl curl \
COPY mailu ./mailu
COPY migrations ./migrations
COPY manage.py .
COPY start.py /start.py
RUN pybabel compile -d mailu/translations
EXPOSE 80/tcp
VOLUME ["/data"]
ENV FLASK_APP mailu
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_sqlalchemy
import flask_bootstrap
import flask_login
import flask_script
import flask_migrate
import flask_babel
import flask_limiter
import os
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)
from mailu import utils, debug, models, manage, configuration
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 mailu import limiter
from mailu import utils
import socket
import flask
@ -19,7 +19,7 @@ def rate_limit_handler(e):
return response
@limiter.request_filter
@utils.limiter.request_filter
def whitelist_webmail():
try:
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 socket

@ -1,5 +1,6 @@
from mailu import db, models, app, limiter
from mailu import models, utils
from mailu.internal import internal, nginx
from flask import current_app as app
import flask
import flask_login
@ -7,7 +8,7 @@ import base64
@internal.route("/auth/email")
@limiter.limit(
@utils.limiter.limit(
app.config["AUTH_RATELIMIT"],
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 flask import current_app as app
import flask
import socket
@ -36,7 +37,7 @@ def dovecot_quota(ns, user_email):
user = models.User.query.get(user_email) or flask.abort(404)
if ns == "storage":
user.quota_bytes_used = flask.request.get_json()
db.session.commit()
models.db.session.commit()
return flask.jsonify(None)

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

@ -1,4 +1,4 @@
from mailu import db, models
from mailu import models
from mailu.internal import internal
import flask
@ -6,7 +6,9 @@ import flask
@internal.route("/postfix/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)
@ -18,37 +20,34 @@ def postfix_mailbox_map(email):
@internal.route("/postfix/alias/<alias>")
def postfix_alias_map(alias):
localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias)
alternative = models.Alternative.query.get(domain)
if alternative:
domain = alternative.domain_name
email = '{}@{}'.format(localpart, domain)
localpart, domain_name = models.Email.resolve_domain(alias)
if localpart is None:
return flask.jsonify(domain)
else:
alias_obj = models.Alias.resolve(localpart, domain)
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)
return flask.jsonify(domain_name)
destination = models.Email.resolve_destination(localpart, domain_name)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/transport/<email>")
def postfix_transport(email):
localpart, domain = email.split('@', 1) if '@' in email else (None, email)
relay = models.Relay.query.get(domain) or flask.abort(404)
if email == '*':
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))
@internal.route("/postfix/sender/<sender>")
def postfix_sender(sender):
@internal.route("/postfix/sender/login/<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
"""
localpart, domain_name = sender.split('@', 1) if '@' in sender else (None, sender)
domain = models.Domain.query.get(domain_name)
alternative = models.Alternative.query.get(domain_name)
if domain or alternative:
return flask.jsonify("REJECT")
return flask.abort(404)
localpart, domain_name = models.Email.resolve_domain(sender)
return flask.jsonify("REJECT") if models.Domain.query.get(domain_name) else 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 socket
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():
""" Advertise this server against statistic services.
"""
@ -23,7 +38,11 @@ def advertise():
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):
""" Create an admin user
"""
@ -41,11 +60,17 @@ def admin(localpart, domain_name, password):
db.session.commit()
@manager.command
def user(localpart, domain_name, password,
hash_scheme=app.config['PASSWORD_SCHEME']):
@mailu.command()
@click.argument('localpart')
@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
"""
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
domain = models.Domain.query.get(domain_name)
if not domain:
domain = models.Domain(name=domain_name)
@ -60,10 +85,12 @@ def user(localpart, domain_name, password,
db.session.commit()
@manager.option('-n', '--domain_name', dest='domain_name')
@manager.option('-u', '--max_users', dest='max_users')
@manager.option('-a', '--max_aliases', dest='max_aliases')
@manager.option('-q', '--max_quota_bytes', dest='max_quota_bytes')
@mailu.command()
@click.option('-n', '--domain_name')
@click.option('-u', '--max_users')
@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):
domain = models.Domain.query.get(domain_name)
if not domain:
@ -72,15 +99,17 @@ def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0):
db.session.commit()
@manager.command
def user_import(localpart, domain_name, password_hash,
hash_scheme=app.config['PASSWORD_SCHEME']):
""" Import a user along with password hash. Available hashes:
'SHA512-CRYPT'
'SHA256-CRYPT'
'MD5-CRYPT'
'CRYPT'
@mailu.command()
@click.argument('localpart')
@click.argument('domain_name')
@click.argument('password_hash')
@click.argument('hash_scheme')
@flask_cli.with_appcontext
def user_import(localpart, domain_name, password_hash, hash_scheme = None):
""" 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)
if not domain:
domain = models.Domain(name=domain_name)
@ -95,7 +124,10 @@ def user_import(localpart, domain_name, password_hash,
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):
"""sync configuration with data from YAML-formatted stdin"""
import yaml
@ -234,7 +266,9 @@ def config_update(verbose=False, delete_objects=False):
db.session.commit()
@manager.command
@mailu.command()
@click.argument('email')
@flask_cli.with_appcontext
def user_delete(email):
"""delete user"""
user = models.User.query.get(email)
@ -243,7 +277,9 @@ def user_delete(email):
db.session.commit()
@manager.command
@mailu.command()
@click.argument('email')
@flask_cli.with_appcontext
def alias_delete(email):
"""delete alias"""
alias = models.Alias.query.get(email)
@ -252,7 +288,11 @@ def alias_delete(email):
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):
""" Create an alias
"""
@ -269,24 +309,31 @@ def alias(localpart, domain_name, destination):
db.session.add(alias)
db.session.commit()
# Set limits to a domain
@manager.command
@mailu.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):
""" Set domain limits
"""
domain = models.Domain.query.get(domain_name)
domain.max_users = max_users
domain.max_aliases = max_aliases
domain.max_quota_bytes = max_quota_bytes
db.session.add(domain)
db.session.commit()
# Make the user manager of a domain
@manager.command
@mailu.command()
@click.argument('domain_name')
@click.argument('user_name')
@flask_cli.with_appcontext
def setmanager(domain_name, user_name='manager'):
""" Make a user manager of a domain
"""
domain = models.Domain.query.get(domain_name)
manageruser = models.User.query.get(user_name + '@' + domain_name)
domain.managers.append(manageruser)
@ -294,5 +341,5 @@ def setmanager(domain_name, user_name='manager'):
db.session.commit()
if __name__ == "__main__":
manager.run()
if __name__ == '__main__':
cli()

@ -1,10 +1,12 @@
from mailu import app, db, dkim, login_manager
from mailu import dkim
from sqlalchemy.ext import declarative
from passlib import context, hash
from datetime import datetime, date
from email.mime import text
from flask import current_app as app
import flask_sqlalchemy
import sqlalchemy
import re
import time
@ -15,6 +17,9 @@ import idna
import dns
db = flask_sqlalchemy.SQLAlchemy()
class IdnaDomain(db.TypeDecorator):
""" Stores a Unicode string in it's IDNA representation (ASCII only)
"""
@ -67,7 +72,28 @@ class CommaSeparatedList(db.TypeDecorator):
return ",".join(value)
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
@ -224,6 +250,28 @@ class Email(object):
msg['To'] = to_address
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):
return self.email
@ -248,7 +296,7 @@ class User(Base, Email):
# Filters
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)
reply_enabled = db.Column(db.Boolean(), nullable=False, default=False)
reply_subject = db.Column(db.String(255), nullable=True, default=None)
@ -296,13 +344,15 @@ class User(Base, Email):
'SHA256-CRYPT': "sha256_crypt",
'MD5-CRYPT': "md5_crypt",
'CRYPT': "des_crypt"}
pw_context = context.CryptContext(
schemes = scheme_dict.values(),
default=scheme_dict[app.config['PASSWORD_SCHEME']],
)
def get_password_context(self):
return context.CryptContext(
schemes=self.scheme_dict.values(),
default=self.scheme_dict[app.config['PASSWORD_SCHEME']],
)
def check_password(self, password):
context = User.pw_context
context = self.get_password_context()
reference = re.match('({[^}]+})?(.*)', self.password).group(2)
result = context.verify(password, reference)
if result and context.identify(reference) != context.default_scheme():
@ -311,15 +361,17 @@ class User(Base, Email):
db.session.commit()
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
@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
if raw:
self.password = '{'+hash_scheme+'}' + password
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):
if self.global_admin:
@ -340,13 +392,15 @@ class User(Base, Email):
self.sendmail(app.config["WELCOME_SUBJECT"],
app.config["WELCOME_BODY"])
@classmethod
def get(cls, email):
return cls.query.get(email)
@classmethod
def login(cls, email, password):
user = cls.query.get(email)
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):
""" 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
import flask

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

@ -14,7 +14,9 @@
{% call macros.box() %}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
{{ 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) }}
{% endcall %}
</form>

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

@ -1,4 +1,4 @@
from mailu import db, models
from mailu import models
from mailu.ui import ui, forms, access
import flask
@ -27,8 +27,8 @@ def alias_create(domain_name):
else:
alias = models.Alias(domain=domain)
form.populate_obj(alias)
db.session.add(alias)
db.session.commit()
models.db.session.add(alias)
models.db.session.commit()
flask.flash('Alias %s created' % alias)
return flask.redirect(
flask.url_for('.alias_list', domain_name=domain.name))
@ -45,7 +45,7 @@ def alias_edit(alias):
form.localpart.validators = []
if form.validate_on_submit():
form.populate_obj(alias)
db.session.commit()
models.db.session.commit()
flask.flash('Alias %s updated' % alias)
return flask.redirect(
flask.url_for('.alias_list', domain_name=alias.domain.name))
@ -59,8 +59,8 @@ def alias_edit(alias):
def alias_delete(alias):
alias = models.Alias.query.get(alias) or flask.abort(404)
domain = alias.domain
db.session.delete(alias)
db.session.commit()
models.db.session.delete(alias)
models.db.session.commit()
flask.flash('Alias %s deleted' % alias)
return flask.redirect(
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
import flask
@ -26,8 +26,8 @@ def alternative_create(domain_name):
else:
alternative = models.Alternative(domain=domain)
form.populate_obj(alternative)
db.session.add(alternative)
db.session.commit()
models.db.session.add(alternative)
models.db.session.commit()
flask.flash('Alternative domain %s created' % alternative)
return flask.redirect(
flask.url_for('.alternative_list', domain_name=domain.name))
@ -41,8 +41,8 @@ def alternative_create(domain_name):
def alternative_delete(alternative):
alternative = models.Alternative.query.get(alternative) or flask.abort(404)
domain = alternative.domain
db.session.delete(alternative)
db.session.commit()
models.db.session.delete(alternative)
models.db.session.commit()
flask.flash('Alternative %s deleted' % alternative)
return flask.redirect(
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
import flask
import flask_login
from urllib import parse
@ui.route('/', methods=["GET"])
@access.authenticated

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

@ -1,4 +1,4 @@
from mailu import db, models
from mailu import models
from mailu.ui import ui, forms, access
import flask
@ -24,8 +24,8 @@ def fetch_create(user_email):
if form.validate_on_submit():
fetch = models.Fetch(user=user)
form.populate_obj(fetch)
db.session.add(fetch)
db.session.commit()
models.db.session.add(fetch)
models.db.session.commit()
flask.flash('Fetch configuration created')
return flask.redirect(
flask.url_for('.fetch_list', user_email=user.email))
@ -39,7 +39,7 @@ def fetch_edit(fetch_id):
form = forms.FetchForm(obj=fetch)
if form.validate_on_submit():
form.populate_obj(fetch)
db.session.commit()
models.db.session.commit()
flask.flash('Fetch configuration updated')
return flask.redirect(
flask.url_for('.fetch_list', user_email=fetch.user.email))
@ -53,8 +53,8 @@ def fetch_edit(fetch_id):
def fetch_delete(fetch_id):
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
user = fetch.user
db.session.delete(fetch)
db.session.commit()
models.db.session.delete(fetch)
models.db.session.commit()
flask.flash('Fetch configuration delete')
return flask.redirect(
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
import flask
@ -30,7 +30,7 @@ def manager_create(domain_name):
flask.flash('User %s is already manager' % user, 'error')
else:
domain.managers.append(user)
db.session.commit()
models.db.session.commit()
flask.flash('User %s can now manage %s' % (user, domain.name))
return flask.redirect(
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)
if user in domain.managers:
domain.managers.remove(user)
db.session.commit()
models.db.session.commit()
flask.flash('User %s can no longer manager %s' % (user, domain))
else:
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
import flask
@ -25,8 +25,8 @@ def relay_create():
else:
relay = models.Relay()
form.populate_obj(relay)
db.session.add(relay)
db.session.commit()
models.db.session.add(relay)
models.db.session.commit()
flask.flash('Relayed domain %s created' % relay)
return flask.redirect(flask.url_for('.relay_list'))
return flask.render_template('relay/create.html', form=form)
@ -41,7 +41,7 @@ def relay_edit(relay_name):
form.name.validators = []
if form.validate_on_submit():
form.populate_obj(relay)
db.session.commit()
models.db.session.commit()
flask.flash('Relayed domain %s saved' % relay)
return flask.redirect(flask.url_for('.relay_list'))
return flask.render_template('relay/edit.html', form=form,
@ -53,8 +53,8 @@ def relay_edit(relay_name):
@access.confirmation_required("delete {relay_name}")
def relay_delete(relay_name):
relay = models.Relay.query.get(relay_name) or flask.abort(404)
db.session.delete(relay)
db.session.commit()
models.db.session.delete(relay)
models.db.session.commit()
flask.flash('Relayed domain %s deleted' % relay)
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 passlib import pwd
@ -32,8 +32,8 @@ def token_create(user_email):
token = models.Token(user=user)
token.set_password(form.raw_password.data)
form.populate_obj(token)
db.session.add(token)
db.session.commit()
models.db.session.add(token)
models.db.session.commit()
flask.flash('Authentication token created')
return flask.redirect(
flask.url_for('.token_list', user_email=user.email))
@ -46,8 +46,8 @@ def token_create(user_email):
def token_delete(token_id):
token = models.Token.query.get(token_id) or flask.abort(404)
user = token.user
db.session.delete(token)
db.session.commit()
models.db.session.delete(token)
models.db.session.commit()
flask.flash('Authentication token deleted')
return flask.redirect(
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 flask import current_app as app
import flask
import flask_login
@ -33,8 +34,8 @@ def user_create(domain_name):
user = models.User(domain=domain)
form.populate_obj(user)
user.set_password(form.pw.data)
db.session.add(user)
db.session.commit()
models.db.session.add(user)
models.db.session.commit()
user.send_welcome()
flask.flash('User %s created' % user)
return flask.redirect(
@ -63,7 +64,7 @@ def user_edit(user_email):
form.populate_obj(user)
if form.pw.data:
user.set_password(form.pw.data)
db.session.commit()
models.db.session.commit()
flask.flash('User %s updated' % user)
return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name))
@ -77,8 +78,8 @@ def user_edit(user_email):
def user_delete(user_email):
user = models.User.query.get(user_email) or flask.abort(404)
domain = user.domain
db.session.delete(user)
db.session.commit()
models.db.session.delete(user)
models.db.session.commit()
flask.flash('User %s deleted' % user)
return flask.redirect(
flask.url_for('.user_list', domain_name=domain.name))
@ -93,7 +94,7 @@ def user_settings(user_email):
form = forms.UserSettingsForm(obj=user)
if form.validate_on_submit():
form.populate_obj(user)
db.session.commit()
models.db.session.commit()
flask.flash('Settings updated for %s' % user)
if user_email:
return flask.redirect(
@ -113,7 +114,7 @@ def user_password(user_email):
flask.flash('Passwords do not match', 'error')
else:
user.set_password(form.pw.data)
db.session.commit()
models.db.session.commit()
flask.flash('Password updated for %s' % user)
if user_email:
return flask.redirect(flask.url_for('.user_list',
@ -130,7 +131,7 @@ def user_forward(user_email):
form = forms.UserForwardForm(obj=user)
if form.validate_on_submit():
form.populate_obj(user)
db.session.commit()
models.db.session.commit()
flask.flash('Forward destination updated for %s' % user)
if user_email:
return flask.redirect(
@ -147,7 +148,7 @@ def user_reply(user_email):
form = forms.UserReplyForm(obj=user)
if form.validate_on_submit():
form.populate_obj(user)
db.session.commit()
models.db.session.commit()
flask.flash('Auto-reply message updated for %s' % user)
if user_email:
return flask.redirect(
@ -170,7 +171,11 @@ def user_signup(domain_name=None):
available_domains=available_domains)
domain = available_domains.get(domain_name) or flask.abort(404)
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 domain.has_email(form.localpart.data):
flask.flash('Email is already used', 'error')
@ -179,8 +184,8 @@ def user_signup(domain_name=None):
form.populate_obj(user)
user.set_password(form.pw.data)
user.quota_bytes = quota_bytes
db.session.add(user)
db.session.commit()
models.db.session.add(user)
models.db.session.commit()
user.send_welcome()
flask.flash('Successfully signed up %s' % user)
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
import sqlalchemy as sa
from mailu import app
fetch_table = sa.Table(
'fetch',
@ -24,13 +22,7 @@ fetch_table = sa.Table(
def upgrade():
connection = op.get_bind()
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():

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

@ -12,7 +12,6 @@ redis
WTForms-Components
passlib
gunicorn
docker-py
tabulate
PyYAML
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
os.system("python3 manage.py advertise")
os.system("python3 manage.py db upgrade")
os.system("gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload mailu:app")
os.system("flask mailu advertise")
os.system("flask db upgrade")
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;
protocol smtp;
smtp_auth plain;
smtp_auth plain login;
}
server {

@ -78,14 +78,14 @@ lmtp_host_lookup = native
smtpd_delay_reject = yes
# 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
smtpd_helo_required = yes
smtpd_client_restrictions =
permit_mynetworks,
check_sender_access ${podop}sender,
check_sender_access ${podop}senderaccess,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_unknown_recipient_domain,

@ -19,7 +19,8 @@ def start_podop():
("alias", "url", "http://admin/internal/postfix/alias/§"),
("domain", "url", "http://admin/internal/postfix/domain/§"),
("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))

@ -15,7 +15,7 @@ alias
.. 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
@ -23,14 +23,14 @@ alias_delete
.. 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
----
.. 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
-----------
@ -39,14 +39,14 @@ primary difference with simple `user` command is that password is being imported
.. 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
------------
.. 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
-------------
@ -55,7 +55,7 @@ The sole purpose of this command is for importing users/aliases in bulk and sync
.. 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:

@ -61,6 +61,7 @@ ANTIVIRUS=none
# Message size limit in bytes
# Default: accept messages up to 50MB
# Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT=50000000
# 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
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
python manage.py db migrate
flask db migrate
This will generate a new script in ``migrations/versions`` that you must review
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
python manage.py db upgrade
flask db upgrade
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 flavors /data/master/flavors
COPY templates /data/master/templates
COPY static ./static
#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
version: '2'
version: '3.6'
services:
redis:
image: redis:alpine
setup:
image: mailu/setup
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}setup:${MAILU_VERSION:-master}
ports:
- "8000:80"
build: .

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

@ -73,6 +73,7 @@ ANTISPAM={{ antispam_enabled or 'none'}}
# Message size limit in bytes
# Default: accept messages up to 50MB
# Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }}
# Networks granted relay permissions, make sure that you include your Docker
@ -144,7 +145,7 @@ DOMAIN_REGISTRATION=true
# 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={{ log_driver or 'json-file' }}
# LOG_DRIVER={{ log_driver or 'json-file' }}
# Docker-compose project name, this will prepended to containers names.
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") %}
<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 }}
docker-compose up -d
docker-compose -p mailu up -d
</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:
<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>
<p>Login to the admin interface to change the password for a safe one, at

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

@ -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>
<div class="form-group">
<label>Enable the admin UI (and path to the admin UI)</label>
<div class="input-group">
<div class="input-group-addon"><input type="checkbox" name="admin_enabled" value="true"></div>
<input class="form-control" type="text" name="admin_path" value="/admin">
</div>
<input type="checkbox" name="admin_enabled" value="true" id="admin">
<label>Enable the admin UI (and path to the admin UI)</label>
<input class="form-control" type="text" name="admin_path" id="admin_path" style="display: none">
</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 %}

@ -15,15 +15,14 @@ accessing messages for beginner users.</p>
<!-- {{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }} -->
<!-- </div> -->
<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"] %}
<option value="{{ webmailtype }}" >{{ webmailtype }}</option>
{% endfor %}
</select>
<p></p>
<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" value="/webmail">
<input class="form-control" type="text" name="webmail_path" id="webmail_path" style="display: none">
</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
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">
<label class="form-check-label">
<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>
</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 %}

@ -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:
front:
image: ${DOCKER_ORG:-mailu}/nginx:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}nginx:${MAILU_VERSION:-local}
build: ../core/nginx
resolver:
image: ${DOCKER_ORG:-mailu}/unbound:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}unbound:${MAILU_VERSION:-local}
build: ../services/unbound
imap:
image: ${DOCKER_ORG:-mailu}/dovecot:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}dovecot:${MAILU_VERSION:-local}
build: ../core/dovecot
smtp:
image: ${DOCKER_ORG:-mailu}/postfix:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}postfix:${MAILU_VERSION:-local}
build: ../core/postfix
antispam:
image: ${DOCKER_ORG:-mailu}/rspamd:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}rspamd:${MAILU_VERSION:-local}
build: ../services/rspamd
antivirus:
image: ${DOCKER_ORG:-mailu}/clamav:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}clamav:${MAILU_VERSION:-local}
build: ../optional/clamav
webdav:
image: ${DOCKER_ORG:-mailu}/radicale:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}radicale:${MAILU_VERSION:-local}
build: ../optional/radicale
admin:
image: ${DOCKER_ORG:-mailu}/admin:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}admin:${MAILU_VERSION:-local}
build: ../core/admin
roundcube:
image: ${DOCKER_ORG:-mailu}/roundcube:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}roundcube:${MAILU_VERSION:-local}
build: ../webmails/roundcube
rainloop:
image: ${DOCKER_ORG:-mailu}/rainloop:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}rainloop:${MAILU_VERSION:-local}
build: ../webmails/rainloop
fetchmail:
image: ${DOCKER_ORG:-mailu}/fetchmail:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}fetchmail:${MAILU_VERSION:-local}
build: ../services/fetchmail
none:
image: ${DOCKER_ORG:-mailu}/none:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}none:${MAILU_VERSION:-local}
build: ../core/none
docs:
image: ${DOCKER_ORG:-mailu}/docs:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}docs:${MAILU_VERSION:-local}
build: ../docs
setup:
image: ${DOCKER_ORG:-mailu}/setup:${VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX}setup:${MAILU_VERSION:-local}
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
#
# Most configuration variables can be modified through the Web interface,
# these few settings must however be configured before starting the mail
# server and require a restart upon change.
# 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
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)
#VERSION=master
# Set to a randomly generated 16 bytes string
SECRET_KEY=ChangeMeChangeMe
SECRET_KEY=HGZCYGVI6FVG31HS
# Address where listening ports should bind
BIND_ADDRESS4=127.0.0.1
#BIND_ADDRESS6=::1
# 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=mail.mailu.io,alternative.mailu.io,yetanother.mailu.io
HOSTNAMES=localhost
# Postmaster local part (will append the main mail domain)
POSTMASTER=admin
@ -44,7 +48,7 @@ DISABLE_STATISTICS=False
###################################
# Expose the admin interface (value: true, false)
ADMIN=false
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=none
@ -53,7 +57,10 @@ WEBMAIL=none
WEBDAV=none
# Antivirus solution (value: clamav, none)
ANTIVIRUS=none
#ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
@ -65,7 +72,7 @@ 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.16.0.0/12
RELAYNETS=172.17.0.0/16
# Will relay all outgoing mails if configured
RELAYHOST=
@ -74,18 +81,12 @@ RELAYHOST=
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
# e.g. localpart+custom@domain;tld
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=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
# choose compression-method, default: none (value: bz2, gz)
@ -109,12 +110,7 @@ SITENAME=Mailu
# Linked Website URL
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
@ -124,17 +120,20 @@ WEBSITE=https://mailu.io
# 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
# LOG_DRIVER=json-file
# 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
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=SHA512-CRYPT
# (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,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
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 default.ini /default.ini

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

@ -1,3 +1,4 @@
date.timezone=UTC
upload_max_filesize = 25M
post_max_size = 25M
upload_max_filesize = {{ MAX_FILESIZE }}M
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["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_/"
shutil.rmtree(base + "domains/", ignore_errors=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("/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")

@ -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
RUN apt-get update && apt-get install -y \
zlib1g-dev \
zlib1g-dev python3-jinja2 \
&& docker-php-ext-install zip \
&& echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini \
&& rm -rf /var/www/html/ \
@ -22,7 +22,7 @@ RUN apt-get update && apt-get install -y \
&& chown -R www-data: logs temp \
&& 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 start.py /start.py

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

@ -1,6 +1,13 @@
#!/usr/bin/python3
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
os.system("mkdir -p /data/gpg")

Loading…
Cancel
Save