Merge branch 'master' into feat-reply-startdate
This commit is contained in:
@@ -17,5 +17,6 @@ COPY start.sh /start.sh
|
||||
RUN pybabel compile -d mailu/translations
|
||||
|
||||
EXPOSE 80/tcp
|
||||
VOLUME ["/data"]
|
||||
|
||||
CMD ["/start.sh"]
|
||||
|
||||
@@ -11,7 +11,6 @@ import os
|
||||
import docker
|
||||
import socket
|
||||
import uuid
|
||||
import redis
|
||||
|
||||
from werkzeug.contrib import fixers
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,7 +6,8 @@ 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):
|
||||
@@ -17,6 +18,7 @@ def rate_limit_handler(e):
|
||||
response.headers['Auth-Wait'] = '3'
|
||||
return response
|
||||
|
||||
|
||||
@limiter.request_filter
|
||||
def whitelist_webmail():
|
||||
try:
|
||||
@@ -26,4 +28,4 @@ def whitelist_webmail():
|
||||
return False
|
||||
|
||||
|
||||
from mailu.internal import views
|
||||
from mailu.internal.views import *
|
||||
|
||||
37
core/admin/mailu/internal/templates/default.sieve
Normal file
37
core/admin/mailu/internal/templates/default.sieve
Normal file
@@ -0,0 +1,37 @@
|
||||
require "variables";
|
||||
require "vacation";
|
||||
require "fileinto";
|
||||
require "envelope";
|
||||
require "mailbox";
|
||||
require "imap4flags";
|
||||
require "regex";
|
||||
require "relational";
|
||||
require "date";
|
||||
require "comparator-i;ascii-numeric";
|
||||
require "spamtestplus";
|
||||
require "editheader";
|
||||
require "index";
|
||||
|
||||
if header :index 2 :matches "Received" "from * by * for <*>; *"
|
||||
{
|
||||
deleteheader "Delivered-To";
|
||||
addheader "Delivered-To" "<${3}>";
|
||||
}
|
||||
|
||||
{% 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 user.reply_active and %}
|
||||
vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
|
||||
{% endif %}
|
||||
3
core/admin/mailu/internal/views/__init__.py
Normal file
3
core/admin/mailu/internal/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
__all__ = [
|
||||
'auth', 'postfix', 'dovecot', 'fetch'
|
||||
]
|
||||
@@ -4,7 +4,6 @@ from mailu.internal import internal, nginx
|
||||
import flask
|
||||
import flask_login
|
||||
import base64
|
||||
import urllib
|
||||
|
||||
|
||||
@internal.route("/auth/email")
|
||||
40
core/admin/mailu/internal/views/dovecot.py
Normal file
40
core/admin/mailu/internal/views/dovecot.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from mailu import db, models
|
||||
from mailu.internal import internal
|
||||
|
||||
import flask
|
||||
|
||||
|
||||
@internal.route("/dovecot/passdb/<user_email>")
|
||||
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/<user_email>")
|
||||
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/<ns>/<user_email>", 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/<script>/<user_email>")
|
||||
def dovecot_sieve_name(script, user_email):
|
||||
return flask.jsonify(script)
|
||||
|
||||
|
||||
@internal.route("/dovecot/sieve/data/default/<user_email>")
|
||||
def dovecot_sieve_data(user_email):
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
return flask.jsonify(flask.render_template("default.sieve", user=user))
|
||||
32
core/admin/mailu/internal/views/fetch.py
Normal file
32
core/admin/mailu/internal/views/fetch.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from mailu import db, models
|
||||
from mailu.internal import internal
|
||||
|
||||
import flask
|
||||
import datetime
|
||||
|
||||
|
||||
@internal.route("/fetch")
|
||||
def fetch_list():
|
||||
return flask.jsonify([
|
||||
{
|
||||
"id": fetch.id,
|
||||
"tls": fetch.tls,
|
||||
"keep": fetch.keep,
|
||||
"user_email": fetch.user_email,
|
||||
"protocol": fetch.protocol,
|
||||
"host": fetch.host,
|
||||
"port": fetch.port,
|
||||
"username": fetch.username,
|
||||
"password": fetch.password
|
||||
} for fetch in models.Fetch.query.all()
|
||||
])
|
||||
|
||||
|
||||
@internal.route("/fetch/<fetch_id>", methods=["POST"])
|
||||
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()
|
||||
return ""
|
||||
54
core/admin/mailu/internal/views/postfix.py
Normal file
54
core/admin/mailu/internal/views/postfix.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from mailu import db, models
|
||||
from mailu.internal import internal
|
||||
|
||||
import flask
|
||||
|
||||
|
||||
@internal.route("/postfix/domain/<domain_name>")
|
||||
def postfix_mailbox_domain(domain_name):
|
||||
domain = models.Domain.query.get(domain_name) or flask.abort(404)
|
||||
return flask.jsonify(domain.name)
|
||||
|
||||
|
||||
@internal.route("/postfix/mailbox/<email>")
|
||||
def postfix_mailbox_map(email):
|
||||
user = models.User.query.get(email) or flask.abort(404)
|
||||
return flask.jsonify(user.email)
|
||||
|
||||
|
||||
@internal.route("/postfix/alias/<alias>")
|
||||
def postfix_alias_map(alias):
|
||||
localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias)
|
||||
alternative = models.Alternative.query.get(domain)
|
||||
if alternative:
|
||||
domain = alternative.domain_name
|
||||
email = '{}@{}'.format(localpart, domain)
|
||||
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)
|
||||
|
||||
|
||||
@internal.route("/postfix/transport/<email>")
|
||||
def postfix_transport(email):
|
||||
localpart, domain = email.split('@', 1) if '@' in email else (None, email)
|
||||
relay = models.Relay.query.get(domain) or flask.abort(404)
|
||||
return flask.jsonify("smtp:[{}]".format(relay.smtp))
|
||||
|
||||
|
||||
@internal.route("/postfix/sender/<sender>")
|
||||
def postfix_sender(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)
|
||||
@@ -1,11 +1,11 @@
|
||||
from mailu import app, db, dkim, login_manager, quota
|
||||
from mailu import app, db, dkim, login_manager
|
||||
|
||||
from sqlalchemy.ext import declarative
|
||||
from passlib import context, hash
|
||||
from datetime import datetime, date
|
||||
from email.mime import text
|
||||
|
||||
|
||||
import sqlalchemy
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
@@ -235,6 +235,7 @@ class User(Base, Email):
|
||||
backref=db.backref('users', cascade='all, delete-orphan'))
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
quota_bytes = db.Column(db.Integer(), nullable=False, default=10**9)
|
||||
quota_bytes_used = db.Column(db.Integer(), nullable=False, default=0)
|
||||
global_admin = db.Column(db.Boolean(), nullable=False, default=False)
|
||||
enabled = db.Column(db.Boolean(), nullable=False, default=True)
|
||||
|
||||
@@ -268,8 +269,23 @@ class User(Base, Email):
|
||||
return self.email
|
||||
|
||||
@property
|
||||
def quota_bytes_used(self):
|
||||
return quota.get(self.email + "/quota/storage") or 0
|
||||
def destination(self):
|
||||
if self.forward_enabled:
|
||||
result = self.self.forward_destination
|
||||
if self.forward_keep:
|
||||
result += ',' + self.email
|
||||
return result
|
||||
else:
|
||||
return self.email
|
||||
|
||||
@property
|
||||
def reply_active(self):
|
||||
now = datetime.datetime.now()
|
||||
return (
|
||||
self.reply_enabled and
|
||||
self.reply_startdate < now and
|
||||
self.reply_enddate > now
|
||||
)
|
||||
|
||||
scheme_dict = {'SHA512-CRYPT': "sha512_crypt",
|
||||
'SHA256-CRYPT': "sha256_crypt",
|
||||
@@ -331,6 +347,22 @@ class Alias(Base, Email):
|
||||
wildcard = db.Column(db.Boolean(), nullable=False, default=False)
|
||||
destination = db.Column(CommaSeparatedList, nullable=False, default=[])
|
||||
|
||||
@classmethod
|
||||
def resolve(cls, localpart, domain_name):
|
||||
return cls.query.filter(
|
||||
sqlalchemy.and_(cls.domain_name == domain_name,
|
||||
sqlalchemy.or_(
|
||||
sqlalchemy.and_(
|
||||
cls.wildcard == False,
|
||||
cls.localpart == localpart
|
||||
), sqlalchemy.and_(
|
||||
cls.wildcard == True,
|
||||
sqlalchemy.bindparam("l", localpart).like(cls.localpart)
|
||||
)
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
|
||||
class Token(Base):
|
||||
""" A token is an application password for a given user.
|
||||
|
||||
28
core/admin/migrations/versions/25fd6c7bcb4a_.py
Normal file
28
core/admin/migrations/versions/25fd6c7bcb4a_.py
Normal file
@@ -0,0 +1,28 @@
|
||||
""" Add a column for used quota
|
||||
|
||||
Revision ID: 25fd6c7bcb4a
|
||||
Revises: 049fed905da7
|
||||
Create Date: 2018-07-25 21:56:09.729153
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '25fd6c7bcb4a'
|
||||
down_revision = '049fed905da7'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('user') as batch:
|
||||
batch.add_column(sa.Column('quota_bytes_used', sa.Integer(), nullable=False, server_default='0'))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('user') as batch:
|
||||
batch.drop_column('user', 'quota_bytes_used')
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
python manage.py advertise
|
||||
python manage.py db upgrade
|
||||
gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload mailu:app
|
||||
gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload mailu:app
|
||||
|
||||
Reference in New Issue
Block a user