diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 00000000..7195e58e --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,10 @@ +rules: + default: null + branches: + master: + protection: + required_status_checks: + contexts: + - continuous-integration/travis-ci + required_pull_request_reviews: + required_approving_review_count: 2 diff --git a/.travis.yml b/.travis.yml index 2ee30837..c3a19529 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,22 @@ -language: python -python: - - "3.6" -install: - - pip install -r docs/requirements.txt +sudo: required +services: docker +addons: + apt: + packages: + - docker-ce +env: + - VERSION=$TRAVIS_BRANCH + script: - - sphinx-versioning build -b -B 1.5 -r 1.5 -w '^[0-9.]*$' -w master -W '^$' docs/ build/ - - python "docs/conf.py" "build" "$DEPLOY_HOST" "$DEPLOY_USERNAME" "$DEPLOY_PASSWORD" "$DEPLOY_REMOTEDIR" + # 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 + +deploy: + provider: script + script: bash tests/deploy.sh + on: + all_branches: true + condition: -n $DOCKER_UN + diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index 0adc626c..2e637206 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -7,7 +7,7 @@ COPY requirements-prod.txt requirements.txt RUN apk add --no-cache openssl \ && apk add --no-cache --virtual build-dep openssl-dev libffi-dev python-dev build-base \ && pip install -r requirements.txt \ - && apk del build-dep + && apk del --no-cache build-dep COPY mailu ./mailu COPY migrations ./migrations @@ -17,5 +17,6 @@ COPY start.sh /start.sh RUN pybabel compile -d mailu/translations EXPOSE 80/tcp +VOLUME ["/data"] CMD ["/start.sh"] diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index a3f0353c..167f04ae 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -11,7 +11,6 @@ import os import docker import socket import uuid -import redis from werkzeug.contrib import fixers @@ -58,7 +57,7 @@ default_config = { 'RECAPTCHA_PUBLIC_KEY': '', 'RECAPTCHA_PRIVATE_KEY': '', # Advanced settings - 'PASSWORD_SCHEME': 'SHA512-CRYPT', + 'PASSWORD_SCHEME': 'BLF-CRYPT', # Host settings 'HOST_IMAP': 'imap', 'HOST_POP3': 'imap', @@ -89,9 +88,6 @@ manager.add_command('db', flask_migrate.MigrateCommand) babel = flask_babel.Babel(app) translations = list(map(str, babel.list_translations())) -# Quota manager -quota = redis.Redis.from_url(app.config.get("QUOTA_STORAGE_URL")) - @babel.localeselector def get_locale(): return flask.request.accept_languages.best_match(translations) diff --git a/core/admin/mailu/internal/__init__.py b/core/admin/mailu/internal/__init__.py index 45084fe5..80a3c754 100644 --- a/core/admin/mailu/internal/__init__.py +++ b/core/admin/mailu/internal/__init__.py @@ -1,10 +1,22 @@ +from flask_limiter import RateLimitExceeded + from mailu import limiter import socket import flask -internal = flask.Blueprint('internal', __name__) +internal = flask.Blueprint('internal', __name__, template_folder='templates') + + +@internal.app_errorhandler(RateLimitExceeded) +def rate_limit_handler(e): + response = flask.Response() + response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded' + response.headers['Auth-Error-Code'] = '451 4.3.2' + if int(flask.request.headers['Auth-Login-Attempt']) < 10: + response.headers['Auth-Wait'] = '3' + return response @limiter.request_filter @@ -16,4 +28,4 @@ def whitelist_webmail(): return False -from mailu.internal import views +from mailu.internal.views import * diff --git a/core/dovecot/sieve/before.sieve b/core/admin/mailu/internal/templates/default.sieve similarity index 55% rename from core/dovecot/sieve/before.sieve rename to core/admin/mailu/internal/templates/default.sieve index 6ebc20c5..5a80a181 100644 --- a/core/dovecot/sieve/before.sieve +++ b/core/admin/mailu/internal/templates/default.sieve @@ -8,8 +8,6 @@ require "regex"; require "relational"; require "date"; require "comparator-i;ascii-numeric"; -require "vnd.dovecot.extdata"; -require "vnd.dovecot.execute"; require "spamtestplus"; require "editheader"; require "index"; @@ -20,21 +18,23 @@ if header :index 2 :matches "Received" "from * by * for <*>; *" addheader "Delivered-To" "<${3}>"; } -if allof (string :is "${extdata.spam_enabled}" "1", - spamtest :percent :value "gt" :comparator "i;ascii-numeric" "${extdata.spam_threshold}") +{% if user.spam_enabled %} +if spamtest :percent :value "gt" :comparator "i;ascii-numeric" "{{ user.spam_threshold }}" { setflag "\\seen"; fileinto :create "Junk"; stop; } +{% endif %} if exists "X-Virus" { discard; stop; } -if allof (string :is "${extdata.reply_enabled}" "1", - currentdate :value "le" "date" "${extdata.reply_enddate}") +{% if user.reply_enabled %} +if currentdate :value "le" "date" "{{ user.reply_enddate }}" { - vacation :days 1 :subject "${extdata.reply_subject}" "${extdata.reply_body}"; + vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}"; } +{% endif %} diff --git a/core/admin/mailu/internal/views/__init__.py b/core/admin/mailu/internal/views/__init__.py new file mode 100644 index 00000000..a32106c0 --- /dev/null +++ b/core/admin/mailu/internal/views/__init__.py @@ -0,0 +1,3 @@ +__all__ = [ + 'auth', 'postfix', 'dovecot', 'fetch' +] diff --git a/core/admin/mailu/internal/views.py b/core/admin/mailu/internal/views/auth.py similarity index 99% rename from core/admin/mailu/internal/views.py rename to core/admin/mailu/internal/views/auth.py index b97d329e..823fbd40 100644 --- a/core/admin/mailu/internal/views.py +++ b/core/admin/mailu/internal/views/auth.py @@ -4,7 +4,6 @@ from mailu.internal import internal, nginx import flask import flask_login import base64 -import urllib @internal.route("/auth/email") diff --git a/core/admin/mailu/internal/views/dovecot.py b/core/admin/mailu/internal/views/dovecot.py new file mode 100644 index 00000000..c2f53794 --- /dev/null +++ b/core/admin/mailu/internal/views/dovecot.py @@ -0,0 +1,40 @@ +from mailu import db, models +from mailu.internal import internal + +import flask + + +@internal.route("/dovecot/passdb/") +def dovecot_passdb_dict(user_email): + user = models.User.query.get(user_email) or flask.abort(404) + return flask.jsonify({ + "password": user.password, + }) + + +@internal.route("/dovecot/userdb/") +def dovecot_userdb_dict(user_email): + user = models.User.query.get(user_email) or flask.abort(404) + return flask.jsonify({ + "quota_rule": "*:bytes={}".format(user.quota_bytes) + }) + + +@internal.route("/dovecot/quota//", methods=["POST"]) +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() + return flask.jsonify(None) + + +@internal.route("/dovecot/sieve/name/