diff --git a/.mergify.yml b/.mergify.yml index 7195e58e..a950b0ca 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -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 diff --git a/.travis.yml b/.travis.yml index c3a19529..22e024ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 - diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index 315b2e39..6fcb4601 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -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 diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index a73e6ab9..6b245c3b 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -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) + diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py new file mode 100644 index 00000000..48599d5e --- /dev/null +++ b/core/admin/mailu/configuration.py @@ -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 diff --git a/core/admin/mailu/debug.py b/core/admin/mailu/debug.py new file mode 100644 index 00000000..7677901b --- /dev/null +++ b/core/admin/mailu/debug.py @@ -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() diff --git a/core/admin/mailu/dockercli.py b/core/admin/mailu/dockercli.py deleted file mode 100644 index 94f71144..00000000 --- a/core/admin/mailu/dockercli.py +++ /dev/null @@ -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) diff --git a/core/admin/mailu/internal/__init__.py b/core/admin/mailu/internal/__init__.py index 80a3c754..fcb19692 100644 --- a/core/admin/mailu/internal/__init__.py +++ b/core/admin/mailu/internal/__init__.py @@ -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"] ==\ diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index cb6bc9cb..460719f2 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -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 diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 823fbd40..459a8e57 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -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"] ) diff --git a/core/admin/mailu/internal/views/dovecot.py b/core/admin/mailu/internal/views/dovecot.py index e6e4c10f..bf2ce2e5 100644 --- a/core/admin/mailu/internal/views/dovecot.py +++ b/core/admin/mailu/internal/views/dovecot.py @@ -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) diff --git a/core/admin/mailu/internal/views/fetch.py b/core/admin/mailu/internal/views/fetch.py index 60249c4b..ccd3a159 100644 --- a/core/admin/mailu/internal/views/fetch.py +++ b/core/admin/mailu/internal/views/fetch.py @@ -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 "" diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 79fbdb8a..5ff31584 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -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/") 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/") 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/") 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/") -def postfix_sender(sender): +@internal.route("/postfix/sender/login/") +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/") +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) diff --git a/core/admin/manage.py b/core/admin/mailu/manage.py similarity index 81% rename from core/admin/manage.py rename to core/admin/mailu/manage.py index f7e9e17d..7f32f6c8 100644 --- a/core/admin/manage.py +++ b/core/admin/mailu/manage.py @@ -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() diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 9a19730f..18e995bf 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -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. diff --git a/core/admin/mailu/ui/access.py b/core/admin/mailu/ui/access.py index 2b6a3767..6a923c8e 100644 --- a/core/admin/mailu/ui/access.py +++ b/core/admin/mailu/ui/access.py @@ -1,4 +1,4 @@ -from mailu import db, models +from mailu import models from mailu.ui import forms import flask diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py index 4f7a30ae..57c106c3 100644 --- a/core/admin/mailu/ui/forms.py +++ b/core/admin/mailu/ui/forms.py @@ -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')) diff --git a/core/admin/mailu/ui/templates/user/signup.html b/core/admin/mailu/ui/templates/user/signup.html index 59c1a988..f540d660 100644 --- a/core/admin/mailu/ui/templates/user/signup.html +++ b/core/admin/mailu/ui/templates/user/signup.html @@ -14,7 +14,9 @@ {% call macros.box() %} {{ macros.form_field(form.localpart, append='@'+domain.name+'') }} {{ 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 %} diff --git a/core/admin/mailu/ui/views/admins.py b/core/admin/mailu/ui/views/admins.py index e9771ec6..7462c545 100644 --- a/core/admin/mailu/ui/views/admins.py +++ b/core/admin/mailu/ui/views/admins.py @@ -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: diff --git a/core/admin/mailu/ui/views/aliases.py b/core/admin/mailu/ui/views/aliases.py index 7fc03266..eaab5cad 100644 --- a/core/admin/mailu/ui/views/aliases.py +++ b/core/admin/mailu/ui/views/aliases.py @@ -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)) diff --git a/core/admin/mailu/ui/views/alternatives.py b/core/admin/mailu/ui/views/alternatives.py index 79acb074..2e6e580b 100644 --- a/core/admin/mailu/ui/views/alternatives.py +++ b/core/admin/mailu/ui/views/alternatives.py @@ -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)) diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index 1ea87401..7501a883 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -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 diff --git a/core/admin/mailu/ui/views/domains.py b/core/admin/mailu/ui/views/domains.py index 7afc1746..719d3844 100644 --- a/core/admin/mailu/ui/views/domains.py +++ b/core/admin/mailu/ui/views/domains.py @@ -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: diff --git a/core/admin/mailu/ui/views/fetches.py b/core/admin/mailu/ui/views/fetches.py index b4ddb824..c0421146 100644 --- a/core/admin/mailu/ui/views/fetches.py +++ b/core/admin/mailu/ui/views/fetches.py @@ -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)) diff --git a/core/admin/mailu/ui/views/managers.py b/core/admin/mailu/ui/views/managers.py index 15ebcd50..45974f94 100644 --- a/core/admin/mailu/ui/views/managers.py +++ b/core/admin/mailu/ui/views/managers.py @@ -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') diff --git a/core/admin/mailu/ui/views/relays.py b/core/admin/mailu/ui/views/relays.py index a0ddc614..f1847798 100644 --- a/core/admin/mailu/ui/views/relays.py +++ b/core/admin/mailu/ui/views/relays.py @@ -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')) diff --git a/core/admin/mailu/ui/views/tokens.py b/core/admin/mailu/ui/views/tokens.py index 4b9881af..3864617b 100644 --- a/core/admin/mailu/ui/views/tokens.py +++ b/core/admin/mailu/ui/views/tokens.py @@ -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)) diff --git a/core/admin/mailu/ui/views/users.py b/core/admin/mailu/ui/views/users.py index c54d22ec..51dfdc13 100644 --- a/core/admin/mailu/ui/views/users.py +++ b/core/admin/mailu/ui/views/users.py @@ -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')) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py new file mode 100644 index 00000000..b11b1689 --- /dev/null +++ b/core/admin/mailu/utils.py @@ -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() diff --git a/core/admin/migrations/versions/3f6994568962_.py b/core/admin/migrations/versions/3f6994568962_.py index ffe8569c..65620c00 100644 --- a/core/admin/migrations/versions/3f6994568962_.py +++ b/core/admin/migrations/versions/3f6994568962_.py @@ -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(): diff --git a/core/admin/migrations/versions/cd79ed46d9c2_.py b/core/admin/migrations/versions/cd79ed46d9c2_.py new file mode 100644 index 00000000..ccf210fe --- /dev/null +++ b/core/admin/migrations/versions/cd79ed46d9c2_.py @@ -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') diff --git a/core/admin/requirements-prod.txt b/core/admin/requirements-prod.txt index e321a4d6..2ca59edc 100644 --- a/core/admin/requirements-prod.txt +++ b/core/admin/requirements-prod.txt @@ -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 diff --git a/core/admin/requirements.txt b/core/admin/requirements.txt index d6e7adb1..95a65bbe 100644 --- a/core/admin/requirements.txt +++ b/core/admin/requirements.txt @@ -12,7 +12,6 @@ redis WTForms-Components passlib gunicorn -docker-py tabulate PyYAML PyOpenSSL diff --git a/core/admin/run.py b/core/admin/run.py deleted file mode 100644 index a55ac1e2..00000000 --- a/core/admin/run.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - - -if __name__ == "__main__": - os.environ["DEBUG"] = "True" - from mailu import app - app.run() diff --git a/core/admin/start.py b/core/admin/start.py index 59c97686..bf0dc38f 100755 --- a/core/admin/start.py +++ b/core/admin/start.py @@ -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()'") diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index 2fb4b8ef..5dc4e274 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -250,7 +250,7 @@ mail { listen 465 ssl; listen [::]:465 ssl; protocol smtp; - smtp_auth plain; + smtp_auth plain login; } server { diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 7db429bb..a67eb433 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -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, diff --git a/core/postfix/start.py b/core/postfix/start.py index b3bb328d..86e9a827 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -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)) diff --git a/docs/cli.rst b/docs/cli.rst index 038f1247..bdf4a6d1 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -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: diff --git a/docs/compose/.env b/docs/compose/.env index 5264b91a..cceeb556 100644 --- a/docs/compose/.env +++ b/docs/compose/.env @@ -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 diff --git a/docs/compose/setup.rst b/docs/compose/setup.rst index 64ad7b25..942a368e 100644 --- a/docs/compose/setup.rst +++ b/docs/compose/setup.rst @@ -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. diff --git a/docs/contributors/database.rst b/docs/contributors/database.rst index ca97f604..a987c980 100644 --- a/docs/contributors/database.rst +++ b/docs/contributors/database.rst @@ -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. diff --git a/setup/Dockerfile b/setup/Dockerfile index 83711af5..e39d7c3b 100644 --- a/setup/Dockerfile +++ b/setup/Dockerfile @@ -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 diff --git a/setup/README.md b/setup/README.md new file mode 100644 index 00000000..24c9cfa2 --- /dev/null +++ b/setup/README.md @@ -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` +``` + +``` + +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 + +``` +

curl {{ url_for('.file', uid=uid, filepath='file.txt', _external=True) }} > file.txt

+``` + diff --git a/setup/docker-compose.yml b/setup/docker-compose.yml index e91332e1..42e7ee18 100644 --- a/setup/docker-compose.yml +++ b/setup/docker-compose.yml @@ -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: . diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index 1a77b1de..93d9dd7d 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -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" diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index c7c60fdf..cdedbea5 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -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' }} diff --git a/setup/flavors/compose/setup.html b/setup/flavors/compose/setup.html index 0379ba82..3d87a263 100644 --- a/setup/flavors/compose/setup.html +++ b/setup/flavors/compose/setup.html @@ -28,15 +28,15 @@ files before going any further.

{% call macros.panel("info", "Step 3 - Start the Compose project") %}

To start your compose project, simply run the Docker Compose up -command.

+command using -p mailu flag for project name.

cd {{ root }}
-docker-compose up -d
+docker-compose -p mailu up -d
 
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: -
docker-compose exec admin python manage.py admin {{ postmaster }} {{ domain }} PASSWORD
+
docker-compose -p mailu exec admin python manage.py admin {{ postmaster }} {{ domain }} PASSWORD
 

Login to the admin interface to change the password for a safe one, at diff --git a/setup/flavors/stack/docker-compose.yml b/setup/flavors/stack/docker-compose.yml index 6e471f39..4e6c3385 100644 --- a/setup/flavors/stack/docker-compose.yml +++ b/setup/flavors/stack/docker-compose.yml @@ -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" diff --git a/setup/static/render.js b/setup/static/render.js new file mode 100644 index 00000000..a1c3fb0d --- /dev/null +++ b/setup/static/render.js @@ -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", ""); + } + }); +}); \ No newline at end of file diff --git a/setup/templates/steps/compose/02_services.html b/setup/templates/steps/compose/02_services.html index 7117c490..11e7a14e 100644 --- a/setup/templates/steps/compose/02_services.html +++ b/setup/templates/steps/compose/02_services.html @@ -15,15 +15,14 @@ accessing messages for beginner users.


- {% for webmailtype in ["none", "roundcube", "rainloop"] %} {% endfor %}

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

-
- -
+ + + + + {% endcall %} diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html index 88c17597..d843d684 100644 --- a/setup/templates/steps/config.html +++ b/setup/templates/steps/config.html @@ -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.

- -
-
- -
+ + +
+ + + + {% endcall %} diff --git a/setup/templates/steps/stack/02_services.html b/setup/templates/steps/stack/02_services.html index 7117c490..36493e05 100644 --- a/setup/templates/steps/stack/02_services.html +++ b/setup/templates/steps/stack/02_services.html @@ -15,15 +15,14 @@ accessing messages for beginner users.


- {% for webmailtype in ["none", "roundcube", "rainloop"] %} {% endfor %}

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

-
- -
+ + + + {% endcall %} diff --git a/setup/templates/steps/stack/04_replicas.html b/setup/templates/steps/stack/04_replicas.html new file mode 100644 index 00000000..785125cc --- /dev/null +++ b/setup/templates/steps/stack/04_replicas.html @@ -0,0 +1,28 @@ +{% call macros.panel("info", "Step 5 - Number of replicas for containers") %} +

Select number of replicas for containers

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +{% endcall %} \ No newline at end of file diff --git a/tests/build.yml b/tests/build.yml index ed5b75fe..a2b4739c 100644 --- a/tests/build.yml +++ b/tests/build.yml @@ -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 diff --git a/tests/certs/cert.pem b/tests/certs/cert.pem new file mode 100644 index 00000000..d6dc928f --- /dev/null +++ b/tests/certs/cert.pem @@ -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----- diff --git a/tests/certs/key.pem b/tests/certs/key.pem new file mode 100644 index 00000000..938633fd --- /dev/null +++ b/tests/certs/key.pem @@ -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----- diff --git a/tests/compose/core/00_create_users.sh b/tests/compose/core/00_create_users.sh new file mode 100755 index 00000000..40d0bd6e --- /dev/null +++ b/tests/compose/core/00_create_users.sh @@ -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!" diff --git a/tests/compose/core/01_email_test.sh b/tests/compose/core/01_email_test.sh new file mode 100755 index 00000000..97dd6e4b --- /dev/null +++ b/tests/compose/core/01_email_test.sh @@ -0,0 +1 @@ +python3 tests/email_test.py message-core \ No newline at end of file diff --git a/tests/compose/core/docker-compose.yml b/tests/compose/core/docker-compose.yml new file mode 100644 index 00000000..397000a6 --- /dev/null +++ b/tests/compose/core/docker-compose.yml @@ -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 diff --git a/tests/compose/core.env b/tests/compose/core/mailu.env similarity index 72% rename from tests/compose/core.env rename to tests/compose/core/mailu.env index 78c307c0..9a744e35 100644 --- a/tests/compose/core.env +++ b/tests/compose/core/mailu.env @@ -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 @@ -34,7 +38,7 @@ POSTMASTER=admin TLS_FLAVOR=cert # Authentication rate limit (per source IP address) -AUTH_RATELIMIT=10/minute;1000/hour +AUTH_RATELIMIT=10/minute;1000/hour # Opt-out of statistics, replace with "True" to opt out DISABLE_STATISTICS=False @@ -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= \ No newline at end of file diff --git a/tests/compose/fetchmail/docker-compose.yml b/tests/compose/fetchmail/docker-compose.yml new file mode 100644 index 00000000..6b1be40e --- /dev/null +++ b/tests/compose/fetchmail/docker-compose.yml @@ -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 diff --git a/tests/compose/fetchmail/mailu.env b/tests/compose/fetchmail/mailu.env new file mode 100644 index 00000000..a987c853 --- /dev/null +++ b/tests/compose/fetchmail/mailu.env @@ -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= \ No newline at end of file diff --git a/tests/compose/filters/01_email_test.sh b/tests/compose/filters/01_email_test.sh new file mode 100755 index 00000000..5af395c4 --- /dev/null +++ b/tests/compose/filters/01_email_test.sh @@ -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 \ No newline at end of file diff --git a/tests/compose/filters/docker-compose.yml b/tests/compose/filters/docker-compose.yml new file mode 100644 index 00000000..4fbda49a --- /dev/null +++ b/tests/compose/filters/docker-compose.yml @@ -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 diff --git a/tests/compose/filters/eicar.com b/tests/compose/filters/eicar.com new file mode 100644 index 00000000..704cac85 --- /dev/null +++ b/tests/compose/filters/eicar.com @@ -0,0 +1 @@ +X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* diff --git a/tests/compose/filters/mailu.env b/tests/compose/filters/mailu.env new file mode 100644 index 00000000..8609a287 --- /dev/null +++ b/tests/compose/filters/mailu.env @@ -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= \ No newline at end of file diff --git a/tests/compose/rainloop/docker-compose.yml b/tests/compose/rainloop/docker-compose.yml new file mode 100644 index 00000000..c91a92ed --- /dev/null +++ b/tests/compose/rainloop/docker-compose.yml @@ -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 diff --git a/tests/compose/rainloop/mailu.env b/tests/compose/rainloop/mailu.env new file mode 100644 index 00000000..678ea048 --- /dev/null +++ b/tests/compose/rainloop/mailu.env @@ -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= \ No newline at end of file diff --git a/tests/compose/roundcube/docker-compose.yml b/tests/compose/roundcube/docker-compose.yml new file mode 100644 index 00000000..567c1c69 --- /dev/null +++ b/tests/compose/roundcube/docker-compose.yml @@ -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 diff --git a/tests/compose/roundcube/mailu.env b/tests/compose/roundcube/mailu.env new file mode 100644 index 00000000..b8a8b266 --- /dev/null +++ b/tests/compose/roundcube/mailu.env @@ -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= \ No newline at end of file diff --git a/tests/compose/run.yml b/tests/compose/run.yml deleted file mode 100644 index eac35b76..00000000 --- a/tests/compose/run.yml +++ /dev/null @@ -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" diff --git a/tests/compose/test-script.sh b/tests/compose/test-script.sh deleted file mode 100755 index 0a3c2237..00000000 --- a/tests/compose/test-script.sh +++ /dev/null @@ -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 - diff --git a/tests/compose/test.py b/tests/compose/test.py new file mode 100755 index 00000000..690855d4 --- /dev/null +++ b/tests/compose/test.py @@ -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) diff --git a/tests/compose/webdav/docker-compose.yml b/tests/compose/webdav/docker-compose.yml new file mode 100644 index 00000000..8e0db6e3 --- /dev/null +++ b/tests/compose/webdav/docker-compose.yml @@ -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 diff --git a/tests/compose/webdav/mailu.env b/tests/compose/webdav/mailu.env new file mode 100644 index 00000000..21dd3981 --- /dev/null +++ b/tests/compose/webdav/mailu.env @@ -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= \ No newline at end of file diff --git a/tests/email_test.py b/tests/email_test.py new file mode 100755 index 00000000..853b76b5 --- /dev/null +++ b/tests/email_test.py @@ -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() diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..36006cfc --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +docker +colorama diff --git a/webmails/rainloop/Dockerfile b/webmails/rainloop/Dockerfile index db7403f5..92479489 100644 --- a/webmails/rainloop/Dockerfile +++ b/webmails/rainloop/Dockerfile @@ -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 diff --git a/webmails/rainloop/config.ini b/webmails/rainloop/config.ini index 7fb13889..6ae5fff7 100644 --- a/webmails/rainloop/config.ini +++ b/webmails/rainloop/config.ini @@ -1,7 +1,7 @@ ; RainLoop Webmail configuration file [webmail] -attachment_size_limit = 25 +attachment_size_limit = {{ MAX_FILESIZE }} [security] allow_admin_panel = Off diff --git a/webmails/rainloop/php.ini b/webmails/rainloop/php.ini index 9b241b46..39abbdd5 100644 --- a/webmails/rainloop/php.ini +++ b/webmails/rainloop/php.ini @@ -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 + diff --git a/webmails/rainloop/start.py b/webmails/rainloop/start.py index 9e8465a2..4c116e09 100755 --- a/webmails/rainloop/start.py +++ b/webmails/rainloop/start.py @@ -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") diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index 14bee56e..00b843b2 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -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 diff --git a/webmails/roundcube/php.ini b/webmails/roundcube/php.ini index 9b241b46..39abbdd5 100644 --- a/webmails/roundcube/php.ini +++ b/webmails/roundcube/php.ini @@ -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 + diff --git a/webmails/roundcube/start.py b/webmails/roundcube/start.py index 07b3a567..3a0bd0bc 100755 --- a/webmails/roundcube/start.py +++ b/webmails/roundcube/start.py @@ -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")