mirror of
https://github.com/Mailu/Mailu.git
synced 2024-12-14 10:53:30 +02:00
First batch of refactoring, using the app factory pattern
This commit is contained in:
parent
7c82be904f
commit
fc24426291
@ -1,132 +1,58 @@
|
||||
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
|
||||
from mailu import utils, debug, db
|
||||
|
||||
# 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_AUTHSMTP': os.environ.get('HOST_SMTP', 'smtp'),
|
||||
}
|
||||
def create_app_from_config(config):
|
||||
""" Create a new application based on the given configuration
|
||||
"""
|
||||
app = flask.Flask(__name__)
|
||||
app.config = config
|
||||
|
||||
# Load configuration from the environment if available
|
||||
for key, value in default_config.items():
|
||||
app.config[key] = os.environ.get(key, value)
|
||||
# Bootstrap is used for basic JS and CSS loading
|
||||
# TODO: remove this and use statically generated assets instead
|
||||
app.bootstrap = flask_bootstrap.Bootstrap(app)
|
||||
|
||||
# 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)
|
||||
# Initialize application extensions
|
||||
models.db.init_app(app)
|
||||
utils.limiter.init_app(app)
|
||||
utils.babel.init_app(app)
|
||||
utils.login.init_app(app)
|
||||
utils.proxy.init_app(app)
|
||||
manage.migrate.init_app(app)
|
||||
manage.manager.init_app(app)
|
||||
|
||||
# Debugging toolbar
|
||||
if app.config.get("DEBUG"):
|
||||
import flask_debugtoolbar
|
||||
toolbar = flask_debugtoolbar.DebugToolbarExtension(app)
|
||||
# Initialize debugging tools
|
||||
if app.config.get("app.debug"):
|
||||
debug.toolbar.init_app(app)
|
||||
debug.profiler.init_app(app)
|
||||
|
||||
# Manager commnad
|
||||
manager = flask_script.Manager(app)
|
||||
manager.add_command('db', flask_migrate.MigrateCommand)
|
||||
# Inject the default variables in the Jinja parser
|
||||
@app.context_processor
|
||||
def inject_defaults():
|
||||
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
|
||||
return dict(
|
||||
current_user=utils.login.current_user,
|
||||
signup_domains=signup_domains,
|
||||
config=app.config
|
||||
)
|
||||
|
||||
# Babel configuration
|
||||
babel = flask_babel.Babel(app)
|
||||
translations = list(map(str, babel.list_translations()))
|
||||
# Import views
|
||||
from mailu import ui, internal
|
||||
app.register_blueprint(ui.ui, url_prefix='/ui')
|
||||
app.register_blueprint(internal.internal, url_prefix='/internal')
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
return flask.request.accept_languages.best_match(translations)
|
||||
return app
|
||||
|
||||
# Login configuration
|
||||
login_manager = flask_login.LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "ui.login"
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def handle_needs_login():
|
||||
return flask.redirect(
|
||||
flask.url_for('ui.login', next=flask.request.endpoint)
|
||||
)
|
||||
|
||||
@app.context_processor
|
||||
def inject_defaults():
|
||||
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
|
||||
return dict(
|
||||
current_user=flask_login.current_user,
|
||||
signup_domains=signup_domains,
|
||||
config=app.config
|
||||
)
|
||||
|
||||
# Import views
|
||||
from mailu import ui, internal
|
||||
app.register_blueprint(ui.ui, url_prefix='/ui')
|
||||
app.register_blueprint(internal.internal, url_prefix='/internal')
|
||||
|
||||
# Create the prefix middleware
|
||||
class PrefixMiddleware(object):
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
prefix = environ.get('HTTP_X_FORWARDED_PREFIX', '')
|
||||
if prefix:
|
||||
environ['SCRIPT_NAME'] = prefix
|
||||
return self.app(environ, start_response)
|
||||
|
||||
app.wsgi_app = PrefixMiddleware(fixers.ProxyFix(app.wsgi_app))
|
||||
def create_app():
|
||||
""" Create a new application based on the config module
|
||||
"""
|
||||
config = configuration.ConfigManager()
|
||||
return create_app_from_config(config)
|
||||
|
70
core/admin/mailu/configuration.py
Normal file
70
core/admin/mailu/configuration.py
Normal file
@ -0,0 +1,70 @@
|
||||
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,
|
||||
# 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(object):
|
||||
""" Naive configuration manager that uses environment only
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = {
|
||||
os.environ.get(key, value)
|
||||
for key, value in DEFAULT_CONFIG.items()
|
||||
}
|
||||
|
||||
def get(self, *args):
|
||||
return self.config.get(*args)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.get(key)
|
17
core/admin/mailu/debug.py
Normal file
17
core/admin/mailu/debug.py
Normal file
@ -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.wsgi_app = werkzeug_profiler.ProfilerMiddleware(
|
||||
app.wsgi_app, restrictions=[30]
|
||||
)
|
||||
|
||||
profiler = Profiler()
|
@ -1,6 +1,6 @@
|
||||
from flask_limiter import RateLimitExceeded
|
||||
|
||||
from mailu import limiter
|
||||
from mailu import utils
|
||||
|
||||
import socket
|
||||
import flask
|
||||
@ -19,7 +19,7 @@ def rate_limit_handler(e):
|
||||
return response
|
||||
|
||||
|
||||
@limiter.request_filter
|
||||
@utils.limiter.request_filter
|
||||
def whitelist_webmail():
|
||||
try:
|
||||
return flask.request.headers["Client-Ip"] ==\
|
||||
|
@ -1,4 +1,5 @@
|
||||
from mailu import db, models, app
|
||||
from mailu import models
|
||||
from flask import current_app as app
|
||||
|
||||
import re
|
||||
import socket
|
||||
|
@ -1,5 +1,6 @@
|
||||
from mailu import db, models, app, limiter
|
||||
from mailu import models, utils
|
||||
from mailu.internal import internal, nginx
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
@ -7,7 +8,7 @@ import base64
|
||||
|
||||
|
||||
@internal.route("/auth/email")
|
||||
@limiter.limit(
|
||||
@utils.limiter.limit(
|
||||
app.config["AUTH_RATELIMIT"],
|
||||
lambda: flask.request.headers["Client-Ip"]
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.internal import internal
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
|
||||
@ -25,7 +26,7 @@ def dovecot_quota(ns, user_email):
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
if ns == "storage":
|
||||
user.quota_bytes_used = flask.request.get_json()
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
return flask.jsonify(None)
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.internal import internal
|
||||
|
||||
import flask
|
||||
@ -27,6 +27,6 @@ def fetch_done(fetch_id):
|
||||
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
|
||||
fetch.last_check = datetime.datetime.now()
|
||||
fetch.error_message = str(flask.request.get_json())
|
||||
db.session.add(fetch)
|
||||
db.session.commit()
|
||||
models.db.session.add(fetch)
|
||||
models.db.session.commit()
|
||||
return ""
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.internal import internal
|
||||
|
||||
import flask
|
||||
|
305
core/admin/mailu/manage.py
Normal file
305
core/admin/mailu/manage.py
Normal file
@ -0,0 +1,305 @@
|
||||
from mailu import models
|
||||
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
import os
|
||||
import socket
|
||||
import uuid
|
||||
|
||||
|
||||
manager = flask_script.Manager()
|
||||
db = models.db
|
||||
|
||||
|
||||
@manager.command
|
||||
def advertise():
|
||||
""" Advertise this server against statistic services.
|
||||
"""
|
||||
if os.path.isfile(app.config["INSTANCE_ID_PATH"]):
|
||||
with open(app.config["INSTANCE_ID_PATH"], "r") as handle:
|
||||
instance_id = handle.read()
|
||||
else:
|
||||
instance_id = str(uuid.uuid4())
|
||||
with open(app.config["INSTANCE_ID_PATH"], "w") as handle:
|
||||
handle.write(instance_id)
|
||||
if app.config["DISABLE_STATISTICS"].lower() != "true":
|
||||
try:
|
||||
socket.gethostbyname(app.config["STATS_ENDPOINT"].format(instance_id))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@manager.command
|
||||
def admin(localpart, domain_name, password):
|
||||
""" Create an admin user
|
||||
"""
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
user = models.User(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
global_admin=True
|
||||
)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@manager.command
|
||||
def user(localpart, domain_name, password,
|
||||
hash_scheme=app.config['PASSWORD_SCHEME']):
|
||||
""" Create a user
|
||||
"""
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
user = models.User(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
global_admin=False
|
||||
)
|
||||
user.set_password(password, hash_scheme=hash_scheme)
|
||||
db.session.add(user)
|
||||
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')
|
||||
def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0):
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
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'
|
||||
"""
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
user = models.User(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
global_admin=False
|
||||
)
|
||||
user.set_password(password_hash, hash_scheme=hash_scheme, raw=True)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@manager.command
|
||||
def config_update(verbose=False, delete_objects=False):
|
||||
"""sync configuration with data from YAML-formatted stdin"""
|
||||
import yaml
|
||||
import sys
|
||||
new_config = yaml.load(sys.stdin)
|
||||
# print new_config
|
||||
domains = new_config.get('domains', [])
|
||||
tracked_domains = set()
|
||||
for domain_config in domains:
|
||||
if verbose:
|
||||
print(str(domain_config))
|
||||
domain_name = domain_config['name']
|
||||
max_users = domain_config.get('max_users', 0)
|
||||
max_aliases = domain_config.get('max_aliases', 0)
|
||||
max_quota_bytes = domain_config.get('max_quota_bytes', 0)
|
||||
tracked_domains.add(domain_name)
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name,
|
||||
max_users=max_users,
|
||||
max_aliases=max_aliases,
|
||||
max_quota_bytes=max_quota_bytes)
|
||||
db.session.add(domain)
|
||||
print("Added " + str(domain_config))
|
||||
else:
|
||||
domain.max_users = max_users
|
||||
domain.max_aliases = max_aliases
|
||||
domain.max_quota_bytes = max_quota_bytes
|
||||
db.session.add(domain)
|
||||
print("Updated " + str(domain_config))
|
||||
|
||||
users = new_config.get('users', [])
|
||||
tracked_users = set()
|
||||
user_optional_params = ('comment', 'quota_bytes', 'global_admin',
|
||||
'enable_imap', 'enable_pop', 'forward_enabled',
|
||||
'forward_destination', 'reply_enabled',
|
||||
'reply_subject', 'reply_body', 'displayed_name',
|
||||
'spam_enabled', 'email', 'spam_threshold')
|
||||
for user_config in users:
|
||||
if verbose:
|
||||
print(str(user_config))
|
||||
localpart = user_config['localpart']
|
||||
domain_name = user_config['domain']
|
||||
password_hash = user_config.get('password_hash', None)
|
||||
hash_scheme = user_config.get('hash_scheme', None)
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
email = '{0}@{1}'.format(localpart, domain_name)
|
||||
optional_params = {}
|
||||
for k in user_optional_params:
|
||||
if k in user_config:
|
||||
optional_params[k] = user_config[k]
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
user = models.User.query.get(email)
|
||||
tracked_users.add(email)
|
||||
tracked_domains.add(domain_name)
|
||||
if not user:
|
||||
user = models.User(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
**optional_params
|
||||
)
|
||||
else:
|
||||
for k in optional_params:
|
||||
setattr(user, k, optional_params[k])
|
||||
user.set_password(password_hash, hash_scheme=hash_scheme, raw=True)
|
||||
db.session.add(user)
|
||||
|
||||
aliases = new_config.get('aliases', [])
|
||||
tracked_aliases = set()
|
||||
for alias_config in aliases:
|
||||
if verbose:
|
||||
print(str(alias_config))
|
||||
localpart = alias_config['localpart']
|
||||
domain_name = alias_config['domain']
|
||||
if type(alias_config['destination']) is str:
|
||||
destination = alias_config['destination'].split(',')
|
||||
else:
|
||||
destination = alias_config['destination']
|
||||
wildcard = alias_config.get('wildcard', False)
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
email = '{0}@{1}'.format(localpart, domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
alias = models.Alias.query.get(email)
|
||||
tracked_aliases.add(email)
|
||||
tracked_domains.add(domain_name)
|
||||
if not alias:
|
||||
alias = models.Alias(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
wildcard=wildcard,
|
||||
destination=destination,
|
||||
email=email
|
||||
)
|
||||
else:
|
||||
alias.destination = destination
|
||||
alias.wildcard = wildcard
|
||||
db.session.add(alias)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
managers = new_config.get('managers', [])
|
||||
# tracked_managers=set()
|
||||
for manager_config in managers:
|
||||
if verbose:
|
||||
print(str(manager_config))
|
||||
domain_name = manager_config['domain']
|
||||
user_name = manager_config['user']
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
manageruser = models.User.query.get(user_name + '@' + domain_name)
|
||||
if manageruser not in domain.managers:
|
||||
domain.managers.append(manageruser)
|
||||
db.session.add(domain)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if delete_objects:
|
||||
for user in db.session.query(models.User).all():
|
||||
if not (user.email in tracked_users):
|
||||
if verbose:
|
||||
print("Deleting user: " + str(user.email))
|
||||
db.session.delete(user)
|
||||
for alias in db.session.query(models.Alias).all():
|
||||
if not (alias.email in tracked_aliases):
|
||||
if verbose:
|
||||
print("Deleting alias: " + str(alias.email))
|
||||
db.session.delete(alias)
|
||||
for domain in db.session.query(models.Domain).all():
|
||||
if not (domain.name in tracked_domains):
|
||||
if verbose:
|
||||
print("Deleting domain: " + str(domain.name))
|
||||
db.session.delete(domain)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@manager.command
|
||||
def user_delete(email):
|
||||
"""delete user"""
|
||||
user = models.User.query.get(email)
|
||||
if user:
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@manager.command
|
||||
def alias_delete(email):
|
||||
"""delete alias"""
|
||||
alias = models.Alias.query.get(email)
|
||||
if alias:
|
||||
db.session.delete(alias)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@manager.command
|
||||
def alias(localpart, domain_name, destination):
|
||||
""" Create an alias
|
||||
"""
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
alias = models.Alias(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
destination=destination.split(','),
|
||||
email="%s@%s" % (localpart, domain_name)
|
||||
)
|
||||
db.session.add(alias)
|
||||
db.session.commit()
|
||||
|
||||
# Set limits to a domain
|
||||
|
||||
|
||||
@manager.command
|
||||
def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
|
||||
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
|
||||
def setmanager(domain_name, user_name='manager'):
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
manageruser = models.User.query.get(user_name + '@' + domain_name)
|
||||
domain.managers.append(manageruser)
|
||||
db.session.add(domain)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager.run()
|
@ -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,6 +72,27 @@ class CommaSeparatedList(db.TypeDecorator):
|
||||
return filter(bool, value.split(","))
|
||||
|
||||
|
||||
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
|
||||
managers = db.Table('manager',
|
||||
db.Column('domain_name', IdnaDomain, db.ForeignKey('domain.name')),
|
||||
@ -324,8 +350,6 @@ class User(Base, Email):
|
||||
user = cls.query.get(email)
|
||||
return user if (user and user.enabled and user.check_password(password)) else None
|
||||
|
||||
login_manager.user_loader(User.query.get)
|
||||
|
||||
|
||||
class Alias(Base, Email):
|
||||
""" An alias is an email address that redirects to some destination.
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import forms
|
||||
|
||||
import flask
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
@ -25,7 +25,7 @@ def admin_create():
|
||||
user = models.User.query.get(form.admin.data)
|
||||
if user:
|
||||
user.global_admin = True
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('User %s is now admin' % user)
|
||||
return flask.redirect(flask.url_for('.admin_list'))
|
||||
else:
|
||||
@ -40,7 +40,7 @@ def admin_delete(admin):
|
||||
user = models.User.query.get(admin)
|
||||
if user:
|
||||
user.global_admin = False
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('User %s is no longer admin' % user)
|
||||
return flask.redirect(flask.url_for('.admin_list'))
|
||||
else:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
@ -27,8 +27,8 @@ def alias_create(domain_name):
|
||||
else:
|
||||
alias = models.Alias(domain=domain)
|
||||
form.populate_obj(alias)
|
||||
db.session.add(alias)
|
||||
db.session.commit()
|
||||
models.db.session.add(alias)
|
||||
models.db.session.commit()
|
||||
flask.flash('Alias %s created' % alias)
|
||||
return flask.redirect(
|
||||
flask.url_for('.alias_list', domain_name=domain.name))
|
||||
@ -45,7 +45,7 @@ def alias_edit(alias):
|
||||
form.localpart.validators = []
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(alias)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Alias %s updated' % alias)
|
||||
return flask.redirect(
|
||||
flask.url_for('.alias_list', domain_name=alias.domain.name))
|
||||
@ -59,8 +59,8 @@ def alias_edit(alias):
|
||||
def alias_delete(alias):
|
||||
alias = models.Alias.query.get(alias) or flask.abort(404)
|
||||
domain = alias.domain
|
||||
db.session.delete(alias)
|
||||
db.session.commit()
|
||||
models.db.session.delete(alias)
|
||||
models.db.session.commit()
|
||||
flask.flash('Alias %s deleted' % alias)
|
||||
return flask.redirect(
|
||||
flask.url_for('.alias_list', domain_name=domain.name))
|
||||
|
@ -26,8 +26,8 @@ def alternative_create(domain_name):
|
||||
else:
|
||||
alternative = models.Alternative(domain=domain)
|
||||
form.populate_obj(alternative)
|
||||
db.session.add(alternative)
|
||||
db.session.commit()
|
||||
models.db.session.add(alternative)
|
||||
models.db.session.commit()
|
||||
flask.flash('Alternative domain %s created' % alternative)
|
||||
return flask.redirect(
|
||||
flask.url_for('.alternative_list', domain_name=domain.name))
|
||||
@ -41,8 +41,8 @@ def alternative_create(domain_name):
|
||||
def alternative_delete(alternative):
|
||||
alternative = models.Alternative.query.get(alternative) or flask.abort(404)
|
||||
domain = alternative.domain
|
||||
db.session.delete(alternative)
|
||||
db.session.commit()
|
||||
models.db.session.delete(alternative)
|
||||
models.db.session.commit()
|
||||
flask.flash('Alternative %s deleted' % alternative)
|
||||
return flask.redirect(
|
||||
flask.url_for('.alternative_list', domain_name=domain.name))
|
||||
|
@ -1,11 +1,9 @@
|
||||
from mailu import dockercli, app, db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
|
||||
from urllib import parse
|
||||
|
||||
|
||||
@ui.route('/', methods=["GET"])
|
||||
@access.authenticated
|
||||
|
@ -1,5 +1,6 @@
|
||||
from mailu import app, db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
@ -26,8 +27,8 @@ def domain_create():
|
||||
else:
|
||||
domain = models.Domain()
|
||||
form.populate_obj(domain)
|
||||
db.session.add(domain)
|
||||
db.session.commit()
|
||||
models.db.session.add(domain)
|
||||
models.db.session.commit()
|
||||
flask.flash('Domain %s created' % domain)
|
||||
return flask.redirect(flask.url_for('.domain_list'))
|
||||
return flask.render_template('domain/create.html', form=form)
|
||||
@ -42,7 +43,7 @@ def domain_edit(domain_name):
|
||||
form.name.validators = []
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(domain)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Domain %s saved' % domain)
|
||||
return flask.redirect(flask.url_for('.domain_list'))
|
||||
return flask.render_template('domain/edit.html', form=form,
|
||||
@ -54,8 +55,8 @@ def domain_edit(domain_name):
|
||||
@access.confirmation_required("delete {domain_name}")
|
||||
def domain_delete(domain_name):
|
||||
domain = models.Domain.query.get(domain_name) or flask.abort(404)
|
||||
db.session.delete(domain)
|
||||
db.session.commit()
|
||||
models.db.session.delete(domain)
|
||||
models.db.session.commit()
|
||||
flask.flash('Domain %s deleted' % domain)
|
||||
return flask.redirect(flask.url_for('.domain_list'))
|
||||
|
||||
@ -99,7 +100,7 @@ def domain_signup(domain_name=None):
|
||||
domain.max_users = 10
|
||||
domain.max_aliases = 10
|
||||
if domain.check_mx():
|
||||
db.session.add(domain)
|
||||
models.db.session.add(domain)
|
||||
if flask_login.current_user.is_authenticated:
|
||||
user = models.User.query.get(flask_login.current_user.email)
|
||||
else:
|
||||
@ -108,9 +109,9 @@ def domain_signup(domain_name=None):
|
||||
form.populate_obj(user)
|
||||
user.set_password(form.pw.data)
|
||||
user.quota_bytes = domain.max_quota_bytes
|
||||
db.session.add(user)
|
||||
models.db.session.add(user)
|
||||
domain.managers.append(user)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Domain %s created' % domain)
|
||||
return flask.redirect(flask.url_for('.domain_list'))
|
||||
else:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
@ -24,8 +24,8 @@ def fetch_create(user_email):
|
||||
if form.validate_on_submit():
|
||||
fetch = models.Fetch(user=user)
|
||||
form.populate_obj(fetch)
|
||||
db.session.add(fetch)
|
||||
db.session.commit()
|
||||
models.db.session.add(fetch)
|
||||
models.db.session.commit()
|
||||
flask.flash('Fetch configuration created')
|
||||
return flask.redirect(
|
||||
flask.url_for('.fetch_list', user_email=user.email))
|
||||
@ -39,7 +39,7 @@ def fetch_edit(fetch_id):
|
||||
form = forms.FetchForm(obj=fetch)
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(fetch)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Fetch configuration updated')
|
||||
return flask.redirect(
|
||||
flask.url_for('.fetch_list', user_email=fetch.user.email))
|
||||
@ -53,8 +53,8 @@ def fetch_edit(fetch_id):
|
||||
def fetch_delete(fetch_id):
|
||||
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
|
||||
user = fetch.user
|
||||
db.session.delete(fetch)
|
||||
db.session.commit()
|
||||
models.db.session.delete(fetch)
|
||||
models.db.session.commit()
|
||||
flask.flash('Fetch configuration delete')
|
||||
return flask.redirect(
|
||||
flask.url_for('.fetch_list', user_email=user.email))
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
@ -30,7 +30,7 @@ def manager_create(domain_name):
|
||||
flask.flash('User %s is already manager' % user, 'error')
|
||||
else:
|
||||
domain.managers.append(user)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('User %s can now manage %s' % (user, domain.name))
|
||||
return flask.redirect(
|
||||
flask.url_for('.manager_list', domain_name=domain.name))
|
||||
@ -46,7 +46,7 @@ def manager_delete(domain_name, user_email):
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
if user in domain.managers:
|
||||
domain.managers.remove(user)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('User %s can no longer manager %s' % (user, domain))
|
||||
else:
|
||||
flask.flash('User %s is not manager' % user, 'error')
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
@ -25,8 +25,8 @@ def relay_create():
|
||||
else:
|
||||
relay = models.Relay()
|
||||
form.populate_obj(relay)
|
||||
db.session.add(relay)
|
||||
db.session.commit()
|
||||
models.db.session.add(relay)
|
||||
models.db.session.commit()
|
||||
flask.flash('Relayed domain %s created' % relay)
|
||||
return flask.redirect(flask.url_for('.relay_list'))
|
||||
return flask.render_template('relay/create.html', form=form)
|
||||
@ -41,7 +41,7 @@ def relay_edit(relay_name):
|
||||
form.name.validators = []
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(relay)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Relayed domain %s saved' % relay)
|
||||
return flask.redirect(flask.url_for('.relay_list'))
|
||||
return flask.render_template('relay/edit.html', form=form,
|
||||
@ -53,8 +53,8 @@ def relay_edit(relay_name):
|
||||
@access.confirmation_required("delete {relay_name}")
|
||||
def relay_delete(relay_name):
|
||||
relay = models.Relay.query.get(relay_name) or flask.abort(404)
|
||||
db.session.delete(relay)
|
||||
db.session.commit()
|
||||
models.db.session.delete(relay)
|
||||
models.db.session.commit()
|
||||
flask.flash('Relayed domain %s deleted' % relay)
|
||||
return flask.redirect(flask.url_for('.relay_list'))
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu import models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
from passlib import pwd
|
||||
@ -32,8 +32,8 @@ def token_create(user_email):
|
||||
token = models.Token(user=user)
|
||||
token.set_password(form.raw_password.data)
|
||||
form.populate_obj(token)
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
models.db.session.add(token)
|
||||
models.db.session.commit()
|
||||
flask.flash('Authentication token created')
|
||||
return flask.redirect(
|
||||
flask.url_for('.token_list', user_email=user.email))
|
||||
@ -46,8 +46,8 @@ def token_create(user_email):
|
||||
def token_delete(token_id):
|
||||
token = models.Token.query.get(token_id) or flask.abort(404)
|
||||
user = token.user
|
||||
db.session.delete(token)
|
||||
db.session.commit()
|
||||
models.db.session.delete(token)
|
||||
models.db.session.commit()
|
||||
flask.flash('Authentication token deleted')
|
||||
return flask.redirect(
|
||||
flask.url_for('.token_list', user_email=user.email))
|
||||
|
@ -1,5 +1,6 @@
|
||||
from mailu import db, models, app
|
||||
from mailu import models
|
||||
from mailu.ui import ui, access, forms
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
@ -33,8 +34,8 @@ def user_create(domain_name):
|
||||
user = models.User(domain=domain)
|
||||
form.populate_obj(user)
|
||||
user.set_password(form.pw.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
models.db.session.add(user)
|
||||
models.db.session.commit()
|
||||
user.send_welcome()
|
||||
flask.flash('User %s created' % user)
|
||||
return flask.redirect(
|
||||
@ -63,7 +64,7 @@ def user_edit(user_email):
|
||||
form.populate_obj(user)
|
||||
if form.pw.data:
|
||||
user.set_password(form.pw.data)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('User %s updated' % user)
|
||||
return flask.redirect(
|
||||
flask.url_for('.user_list', domain_name=user.domain.name))
|
||||
@ -77,8 +78,8 @@ def user_edit(user_email):
|
||||
def user_delete(user_email):
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
domain = user.domain
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
models.db.session.delete(user)
|
||||
models.db.session.commit()
|
||||
flask.flash('User %s deleted' % user)
|
||||
return flask.redirect(
|
||||
flask.url_for('.user_list', domain_name=domain.name))
|
||||
@ -93,7 +94,7 @@ def user_settings(user_email):
|
||||
form = forms.UserSettingsForm(obj=user)
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(user)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Settings updated for %s' % user)
|
||||
if user_email:
|
||||
return flask.redirect(
|
||||
@ -113,7 +114,7 @@ def user_password(user_email):
|
||||
flask.flash('Passwords do not match', 'error')
|
||||
else:
|
||||
user.set_password(form.pw.data)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Password updated for %s' % user)
|
||||
if user_email:
|
||||
return flask.redirect(flask.url_for('.user_list',
|
||||
@ -130,7 +131,7 @@ def user_forward(user_email):
|
||||
form = forms.UserForwardForm(obj=user)
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(user)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Forward destination updated for %s' % user)
|
||||
if user_email:
|
||||
return flask.redirect(
|
||||
@ -147,7 +148,7 @@ def user_reply(user_email):
|
||||
form = forms.UserReplyForm(obj=user)
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(user)
|
||||
db.session.commit()
|
||||
models.db.session.commit()
|
||||
flask.flash('Auto-reply message updated for %s' % user)
|
||||
if user_email:
|
||||
return flask.redirect(
|
||||
@ -179,8 +180,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'))
|
||||
|
46
core/admin/mailu/utils.py
Normal file
46
core/admin/mailu/utils.py
Normal file
@ -0,0 +1,46 @@
|
||||
import flask
|
||||
import flask_login
|
||||
import flask_script
|
||||
import flask_migrate
|
||||
import flask_babel
|
||||
import flask_limiter
|
||||
|
||||
|
||||
# Login configuration
|
||||
login = flask_login.LoginManager()
|
||||
login.login_view = "ui.login"
|
||||
login.user_loader(models.User.query.get)
|
||||
|
||||
@login_manager.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()
|
Loading…
Reference in New Issue
Block a user