Clean most of the refactored code

master
kaiyou 6 years ago
parent f40fcd7ac0
commit 82069ea3f0

@ -1,44 +1,40 @@
import flask import flask
import flask_bootstrap import flask_bootstrap
import os from mailu import utils, debug, models, configuration
import docker
import socket
import uuid
from mailu import utils, debug, db
def create_app_from_config(config): def create_app_from_config(config):
""" Create a new application based on the given configuration """ Create a new application based on the given configuration
""" """
app = flask.Flask(__name__) app = flask.Flask(__name__)
app.config = config app.app_context().push()
# Bootstrap is used for basic JS and CSS loading # Bootstrap is used for basic JS and CSS loading
# TODO: remove this and use statically generated assets instead # TODO: remove this and use statically generated assets instead
app.bootstrap = flask_bootstrap.Bootstrap(app) app.bootstrap = flask_bootstrap.Bootstrap(app)
# Initialize application extensions # Initialize application extensions
config.init_app(app)
models.db.init_app(app) models.db.init_app(app)
utils.limiter.init_app(app) utils.limiter.init_app(app)
utils.babel.init_app(app) utils.babel.init_app(app)
utils.login.init_app(app) utils.login.init_app(app)
utils.login.user_loader(models.User.query.get)
utils.proxy.init_app(app) utils.proxy.init_app(app)
manage.migrate.init_app(app)
manage.manager.init_app(app)
# Initialize debugging tools # Initialize debugging tools
if app.config.get("app.debug"): if app.config.get("DEBUG"):
debug.toolbar.init_app(app) debug.toolbar.init_app(app)
debug.profiler.init_app(app) # TODO: add a specific configuration variable for profiling
# debug.profiler.init_app(app)
# Inject the default variables in the Jinja parser # Inject the default variables in the Jinja parser
# TODO: move this to blueprints when needed
@app.context_processor @app.context_processor
def inject_defaults(): def inject_defaults():
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all() signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
return dict( return dict(
current_user=utils.login.current_user,
signup_domains=signup_domains, signup_domains=signup_domains,
config=app.config config=app.config
) )

@ -4,7 +4,7 @@ import os
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
# Specific to the admin UI # Specific to the admin UI
'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/main.db', 'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/main.db',
'SQLALCHEMY_TRACK_MODIFICATIONS': False, 'SQLALCHEMY_TRACK_MODIFICATIONS': True,
'DOCKER_SOCKET': 'unix:///var/run/docker.sock', 'DOCKER_SOCKET': 'unix:///var/run/docker.sock',
'BABEL_DEFAULT_LOCALE': 'en', 'BABEL_DEFAULT_LOCALE': 'en',
'BABEL_DEFAULT_TIMEZONE': 'UTC', 'BABEL_DEFAULT_TIMEZONE': 'UTC',
@ -13,6 +13,7 @@ DEFAULT_CONFIG = {
'QUOTA_STORAGE_URL': 'redis://redis/1', 'QUOTA_STORAGE_URL': 'redis://redis/1',
'DEBUG': False, 'DEBUG': False,
'DOMAIN_REGISTRATION': False, 'DOMAIN_REGISTRATION': False,
'TEMPLATES_AUTO_RELOAD': True,
# Statistics management # Statistics management
'INSTANCE_ID_PATH': '/data/instance', 'INSTANCE_ID_PATH': '/data/instance',
'STATS_ENDPOINT': '0.{}.stats.mailu.io', 'STATS_ENDPOINT': '0.{}.stats.mailu.io',
@ -58,13 +59,29 @@ class ConfigManager(object):
""" """
def __init__(self): def __init__(self):
self.config = { self.config = dict()
os.environ.get(key, value)
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() 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): def get(self, *args):
return self.config.get(*args) return self.config.get(*args)
def __getitem__(self, key): def __getitem__(self, key):
return self.get(key) return self.config.get(key)
def __setitem__(self, key, value):
self.config[key] = value
def __contains__(self, key):
return key in self.config

@ -9,7 +9,7 @@ toolbar = flask_debugtoolbar.DebugToolbarExtension()
# Profiler # Profiler
class Profiler(object): class Profiler(object):
def init_app(self): def init_app(self, app):
app.wsgi_app = werkzeug_profiler.ProfilerMiddleware( app.wsgi_app = werkzeug_profiler.ProfilerMiddleware(
app.wsgi_app, restrictions=[30] app.wsgi_app, restrictions=[30]
) )

@ -14,12 +14,13 @@ db = models.db
@click.group() @click.group()
def cli(cls=flask_cli.FlaskGroup, create_app=mailu.create_app): def cli(cls=flask_cli.FlaskGroup, create_app=create_app):
""" Main command group """ Main command group
""" """
@cli.command() @cli.command()
@flask_cli.with_appcontext
def advertise(): def advertise():
""" Advertise this server against statistic services. """ Advertise this server against statistic services.
""" """
@ -38,9 +39,10 @@ def advertise():
@cli.command() @cli.command()
@cli.argument('localpart', help='localpart for the new admin') @click.argument('localpart')
@cli.argument('domain_name', help='domain name for the new admin') @click.argument('domain_name')
@cli.argument('password', help='plain password for the new admin') @click.argument('password')
@flask_cli.with_appcontext
def admin(localpart, domain_name, password): def admin(localpart, domain_name, password):
""" Create an admin user """ Create an admin user
""" """
@ -59,14 +61,16 @@ def admin(localpart, domain_name, password):
@cli.command() @cli.command()
@cli.argument('localpart', help='localpart for the new user') @click.argument('localpart')
@cli.argument('domain_name', help='domain name for the new user') @click.argument('domain_name')
@cli.argument('password', help='plain password for the new user') @click.argument('password')
@cli.argument('hash_scheme', help='password hashing scheme') @click.argument('hash_scheme')
def user(localpart, domain_name, password, @flask_cli.with_appcontext
hash_scheme=app.config['PASSWORD_SCHEME']): def user(localpart, domain_name, password, hash_scheme=None):
""" Create a user """ Create a user
""" """
if hash_scheme is None:
hash_scheme = app.config['PASSWORD_SCHEME']
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
@ -82,10 +86,11 @@ def user(localpart, domain_name, password,
@cli.command() @cli.command()
@cli.option('-n', '--domain_name', dest='domain_name') @click.option('-n', '--domain_name')
@cli.option('-u', '--max_users', dest='max_users') @click.option('-u', '--max_users')
@cli.option('-a', '--max_aliases', dest='max_aliases') @click.option('-a', '--max_aliases')
@cli.option('-q', '--max_quota_bytes', dest='max_quota_bytes') @click.option('-q', '--max_quota_bytes')
@flask_cli.with_appcontext
def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0): def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0):
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
@ -95,14 +100,16 @@ def domain(domain_name, max_users=0, max_aliases=0, max_quota_bytes=0):
@cli.command() @cli.command()
@cli.argument('localpart', help='localpart for the new user') @click.argument('localpart')
@cli.argument('domain_name', help='domain name for the new user') @click.argument('domain_name')
@cli.argument('password_hash', help='password hash for the new user') @click.argument('password_hash')
@cli.argument('hash_scheme', help='password hashing scheme') @click.argument('hash_scheme')
def user_import(localpart, domain_name, password_hash, @flask_cli.with_appcontext
hash_scheme=app.config['PASSWORD_SCHEME']): def user_import(localpart, domain_name, password_hash, hash_scheme = None):
""" Import a user along with password hash. """ 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) domain = models.Domain.query.get(domain_name)
if not domain: if not domain:
domain = models.Domain(name=domain_name) domain = models.Domain(name=domain_name)
@ -118,8 +125,9 @@ def user_import(localpart, domain_name, password_hash,
@cli.command() @cli.command()
@cli.option('-v', dest='verbose') @click.option('-v', '--verbose')
@cli.option('-d', dest='delete_objects') @click.option('-d', '--delete_objects')
@flask_cli.with_appcontext
def config_update(verbose=False, delete_objects=False): def config_update(verbose=False, delete_objects=False):
"""sync configuration with data from YAML-formatted stdin""" """sync configuration with data from YAML-formatted stdin"""
import yaml import yaml
@ -259,7 +267,8 @@ def config_update(verbose=False, delete_objects=False):
@cli.command() @cli.command()
@cli.argument('email', help='email address to be deleted') @click.argument('email')
@flask_cli.with_appcontext
def user_delete(email): def user_delete(email):
"""delete user""" """delete user"""
user = models.User.query.get(email) user = models.User.query.get(email)
@ -269,7 +278,8 @@ def user_delete(email):
@cli.command() @cli.command()
@cli.argument('email', help='email alias to be deleted') @click.argument('email')
@flask_cli.with_appcontext
def alias_delete(email): def alias_delete(email):
"""delete alias""" """delete alias"""
alias = models.Alias.query.get(email) alias = models.Alias.query.get(email)
@ -279,9 +289,10 @@ def alias_delete(email):
@cli.command() @cli.command()
@cli.argument('localpart', help='localpart for the new alias') @click.argument('localpart')
@cli.argument('domain_name', help='domain name for the new alias') @click.argument('domain_name')
@cli.argument('destination', help='destination for the new alias') @click.argument('destination')
@flask_cli.with_appcontext
def alias(localpart, domain_name, destination): def alias(localpart, domain_name, destination):
""" Create an alias """ Create an alias
""" """
@ -300,10 +311,11 @@ def alias(localpart, domain_name, destination):
@cli.command() @cli.command()
@cli.argument('domain_name', help='domain to be updated') @click.argument('domain_name')
@cli.argument('max_users', help='maximum user count') @click.argument('max_users')
@cli.argument('max_aliases', help='maximum alias count') @click.argument('max_aliases')
@cli.argument('max_quota_bytes', help='maximum quota bytes par user') @click.argument('max_quota_bytes')
@flask_cli.with_appcontext
def setlimits(domain_name, max_users, max_aliases, max_quota_bytes): def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
""" Set domain limits """ Set domain limits
""" """
@ -316,8 +328,9 @@ def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
@cli.command() @cli.command()
@cli.argument('domain_name', help='target domain name') @click.argument('domain_name')
@cli.argument('user_name', help='username inside the target domain') @click.argument('user_name')
@flask_cli.with_appcontext
def setmanager(domain_name, user_name='manager'): def setmanager(domain_name, user_name='manager'):
""" Make a user manager of a domain """ Make a user manager of a domain
""" """
@ -327,3 +340,6 @@ def setmanager(domain_name, user_name='manager'):
db.session.add(domain) db.session.add(domain)
db.session.commit() db.session.commit()
if __name__ == '__main__':
cli()

@ -307,24 +307,28 @@ class User(Base, Email):
'SHA256-CRYPT': "sha256_crypt", 'SHA256-CRYPT': "sha256_crypt",
'MD5-CRYPT': "md5_crypt", 'MD5-CRYPT': "md5_crypt",
'CRYPT': "des_crypt"} 'CRYPT': "des_crypt"}
pw_context = context.CryptContext(
schemes = scheme_dict.values(), def get_password_context(self):
default=scheme_dict[app.config['PASSWORD_SCHEME']], return context.CryptContext(
schemes=self.scheme_dict.values(),
default=self.scheme_dict[app.config['PASSWORD_SCHEME']],
) )
def check_password(self, password): def check_password(self, password):
reference = re.match('({[^}]+})?(.*)', self.password).group(2) reference = re.match('({[^}]+})?(.*)', self.password).group(2)
return User.pw_context.verify(password, reference) return self.get_password_context().verify(password, reference)
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 """Set password for user with specified encryption scheme
@password: plain text password to encrypt (if raw == True the hash itself) @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 # for the list of hash schemes see https://wiki2.dovecot.org/Authentication/PasswordSchemes
if raw: if raw:
self.password = '{'+hash_scheme+'}' + password self.password = '{'+hash_scheme+'}' + password
else: 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): def get_managed_domains(self):
if self.global_admin: if self.global_admin:

@ -1,4 +1,4 @@
from mailu import db, models from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
import flask import flask

@ -1,3 +1,5 @@
from mailu import models
import flask import flask
import flask_login import flask_login
import flask_script import flask_script
@ -5,13 +7,14 @@ import flask_migrate
import flask_babel import flask_babel
import flask_limiter import flask_limiter
from werkzeug.contrib import fixers
# Login configuration # Login configuration
login = flask_login.LoginManager() login = flask_login.LoginManager()
login.login_view = "ui.login" login.login_view = "ui.login"
login.user_loader(models.User.query.get)
@login_manager.unauthorized_handler @login.unauthorized_handler
def handle_needs_login(): def handle_needs_login():
return flask.redirect( return flask.redirect(
flask.url_for('ui.login', next=flask.request.endpoint) flask.url_for('ui.login', next=flask.request.endpoint)

@ -1,298 +0,0 @@
from mailu import app, manager, db, models
import os
import socket
import uuid
@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()
Loading…
Cancel
Save