cosmetic changes & make linter happy

renamed single letter variables (m => match)
renamed classmethod arguments to cls (model)
removed shadowing of variables (hash, context)
shortened unneeded lambda functions (id)
converted type ... is to isinstance(...)
removed unneded imports (flask)
master
Alexander Graf 4 years ago
parent 7229c89de1
commit 4c258f5a6b

@ -1,9 +1,8 @@
from mailu import models from mailu import models
from flask import current_app as app from flask import current_app as app
from flask import cli as flask_cli from flask.cli import FlaskGroup, with_appcontext
import flask
import os import os
import socket import socket
import uuid import uuid
@ -15,14 +14,14 @@ import sys
db = models.db db = models.db
@click.group() @click.group(cls=FlaskGroup)
def mailu(cls=flask_cli.FlaskGroup): def mailu():
""" Mailu command line """ Mailu command line
""" """
@mailu.command() @mailu.command()
@flask_cli.with_appcontext @with_appcontext
def advertise(): def advertise():
""" Advertise this server against statistic services. """ Advertise this server against statistic services.
""" """
@ -45,7 +44,7 @@ def advertise():
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password') @click.argument('password')
@click.option('-m', '--mode') @click.option('-m', '--mode')
@flask_cli.with_appcontext @with_appcontext
def admin(localpart, domain_name, password, mode='create'): def admin(localpart, domain_name, password, mode='create'):
""" Create an admin user """ Create an admin user
'mode' can be: 'mode' can be:
@ -89,7 +88,7 @@ def admin(localpart, domain_name, password, mode='create'):
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password') @click.argument('password')
@click.argument('hash_scheme', required=False) @click.argument('hash_scheme', required=False)
@flask_cli.with_appcontext @with_appcontext
def user(localpart, domain_name, password, hash_scheme=None): def user(localpart, domain_name, password, hash_scheme=None):
""" Create a user """ Create a user
""" """
@ -114,7 +113,7 @@ def user(localpart, domain_name, password, hash_scheme=None):
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password') @click.argument('password')
@click.argument('hash_scheme', required=False) @click.argument('hash_scheme', required=False)
@flask_cli.with_appcontext @with_appcontext
def password(localpart, domain_name, password, hash_scheme=None): def password(localpart, domain_name, password, hash_scheme=None):
""" Change the password of an user """ Change the password of an user
""" """
@ -134,7 +133,7 @@ def password(localpart, domain_name, password, hash_scheme=None):
@click.option('-u', '--max-users') @click.option('-u', '--max-users')
@click.option('-a', '--max-aliases') @click.option('-a', '--max-aliases')
@click.option('-q', '--max-quota-bytes') @click.option('-q', '--max-quota-bytes')
@flask_cli.with_appcontext @with_appcontext
def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0): def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0):
""" Create a domain """ Create a domain
""" """
@ -151,7 +150,7 @@ def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0):
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('password_hash') @click.argument('password_hash')
@click.argument('hash_scheme') @click.argument('hash_scheme')
@flask_cli.with_appcontext @with_appcontext
def user_import(localpart, domain_name, password_hash, hash_scheme = None): 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.
""" """
@ -183,7 +182,7 @@ yaml_sections = [
@click.option('-v', '--verbose', is_flag=True, help='Increase verbosity') @click.option('-v', '--verbose', is_flag=True, help='Increase verbosity')
@click.option('-d', '--delete-objects', is_flag=True, help='Remove objects not included in yaml') @click.option('-d', '--delete-objects', is_flag=True, help='Remove objects not included in yaml')
@click.option('-n', '--dry-run', is_flag=True, help='Perform a trial run with no changes made') @click.option('-n', '--dry-run', is_flag=True, help='Perform a trial run with no changes made')
@flask_cli.with_appcontext @with_appcontext
def config_update(verbose=False, delete_objects=False, dry_run=False, file=None): def config_update(verbose=False, delete_objects=False, dry_run=False, file=None):
"""sync configuration with data from YAML-formatted stdin""" """sync configuration with data from YAML-formatted stdin"""
@ -303,7 +302,7 @@ def config_update(verbose=False, delete_objects=False, dry_run=False, file=None)
@click.option('-s', '--secrets', is_flag=True, help='Include secrets (dkim-key, plain-text / not hashed)') @click.option('-s', '--secrets', is_flag=True, help='Include secrets (dkim-key, plain-text / not hashed)')
@click.option('-d', '--dns', is_flag=True, help='Include dns records') @click.option('-d', '--dns', is_flag=True, help='Include dns records')
@click.argument('sections', nargs=-1) @click.argument('sections', nargs=-1)
@flask_cli.with_appcontext @with_appcontext
def config_dump(full=False, secrets=False, dns=False, sections=None): def config_dump(full=False, secrets=False, dns=False, sections=None):
"""dump configuration as YAML-formatted data to stdout """dump configuration as YAML-formatted data to stdout
@ -343,7 +342,7 @@ def config_dump(full=False, secrets=False, dns=False, sections=None):
@mailu.command() @mailu.command()
@click.argument('email') @click.argument('email')
@flask_cli.with_appcontext @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)
@ -354,7 +353,7 @@ def user_delete(email):
@mailu.command() @mailu.command()
@click.argument('email') @click.argument('email')
@flask_cli.with_appcontext @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)
@ -368,7 +367,7 @@ def alias_delete(email):
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('destination') @click.argument('destination')
@click.option('-w', '--wildcard', is_flag=True) @click.option('-w', '--wildcard', is_flag=True)
@flask_cli.with_appcontext @with_appcontext
def alias(localpart, domain_name, destination, wildcard=False): def alias(localpart, domain_name, destination, wildcard=False):
""" Create an alias """ Create an alias
""" """
@ -392,7 +391,7 @@ def alias(localpart, domain_name, destination, wildcard=False):
@click.argument('max_users') @click.argument('max_users')
@click.argument('max_aliases') @click.argument('max_aliases')
@click.argument('max_quota_bytes') @click.argument('max_quota_bytes')
@flask_cli.with_appcontext @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
""" """
@ -407,7 +406,7 @@ def setlimits(domain_name, max_users, max_aliases, max_quota_bytes):
@mailu.command() @mailu.command()
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('user_name') @click.argument('user_name')
@flask_cli.with_appcontext @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
""" """

@ -1,7 +1,6 @@
from mailu import dkim from mailu import dkim
from sqlalchemy.ext import declarative from sqlalchemy.ext import declarative
from passlib import context, hash
from datetime import datetime, date from datetime import datetime, date
from email.mime import text from email.mime import text
from flask import current_app as app from flask import current_app as app
@ -12,6 +11,7 @@ import sqlalchemy
import re import re
import time import time
import os import os
import passlib
import glob import glob
import smtplib import smtplib
import idna import idna
@ -113,8 +113,8 @@ class Base(db.Model):
comment = db.Column(db.String(255), nullable=True) comment = db.Column(db.String(255), nullable=True)
@classmethod @classmethod
def _dict_pkey(model): def _dict_pkey(cls):
return model.__mapper__.primary_key[0].name return cls.__mapper__.primary_key[0].name
def _dict_pval(self): def _dict_pval(self):
return getattr(self, self._dict_pkey()) return getattr(self, self._dict_pkey())
@ -187,57 +187,57 @@ class Base(db.Model):
return res return res
@classmethod @classmethod
def from_dict(model, data, delete=False): def from_dict(cls, data, delete=False):
changed = [] changed = []
pkey = model._dict_pkey() pkey = cls._dict_pkey()
# handle "primary key" only # handle "primary key" only
if type(data) is not dict: if isinstance(data, dict):
data = {pkey: data} data = {pkey: data}
# modify input data # modify input data
if hasattr(model, '_dict_input'): if hasattr(cls, '_dict_input'):
try: try:
model._dict_input(data) cls._dict_input(data)
except Exception as reason: except Exception as reason:
raise ValueError(f'{reason}', model, None, data) raise ValueError(f'{reason}', cls, None, data)
# check for primary key (if not recursed) # check for primary key (if not recursed)
if not getattr(model, '_dict_recurse', False): if not getattr(cls, '_dict_recurse', False):
if not pkey in data: if not pkey in data:
raise KeyError(f'primary key {model.__table__}.{pkey} is missing', model, pkey, data) raise KeyError(f'primary key {cls.__table__}.{pkey} is missing', cls, pkey, data)
# check data keys and values # check data keys and values
for key in list(data.keys()): for key in list(data.keys()):
# check key # check key
if not hasattr(model, key) and not key in model.__mapper__.relationships: if not hasattr(cls, key) and not key in cls.__mapper__.relationships:
raise KeyError(f'unknown key {model.__table__}.{key}', model, key, data) raise KeyError(f'unknown key {cls.__table__}.{key}', cls, key, data)
# check value type # check value type
value = data[key] value = data[key]
col = model.__mapper__.columns.get(key) col = cls.__mapper__.columns.get(key)
if col is not None: if col is not None:
if not ((value is None and col.nullable) or (type(value) is col.type.python_type)): if not ((value is None and col.nullable) or (isinstance(value, col.type.python_type))):
raise TypeError(f'{model.__table__}.{key} {value!r} has invalid type {type(value).__name__!r}', model, key, data) raise TypeError(f'{cls.__table__}.{key} {value!r} has invalid type {type(value).__name__!r}', cls, key, data)
else: else:
rel = model.__mapper__.relationships.get(key) rel = cls.__mapper__.relationships.get(key)
if rel is None: if rel is None:
itype = getattr(model, '_dict_types', {}).get(key) itype = getattr(cls, '_dict_types', {}).get(key)
if itype is not None: if itype is not None:
if itype is False: # ignore value. TODO: emit warning? if itype is False: # ignore value. TODO: emit warning?
del data[key] del data[key]
continue continue
elif not isinstance(value, itype): elif not isinstance(value, itype):
raise TypeError(f'{model.__table__}.{key} {value!r} has invalid type {type(value).__name__!r}', model, key, data) raise TypeError(f'{cls.__table__}.{key} {value!r} has invalid type {type(value).__name__!r}', cls, key, data)
else: else:
raise NotImplementedError(f'type not defined for {model.__table__}.{key}') raise NotImplementedError(f'type not defined for {cls.__table__}.{key}')
# handle relationships # handle relationships
if key in model.__mapper__.relationships: if key in cls.__mapper__.relationships:
rel_model = model.__mapper__.relationships[key].argument rel_model = cls.__mapper__.relationships[key].argument
if not isinstance(rel_model, sqlalchemy.orm.Mapper): if not isinstance(rel_model, sqlalchemy.orm.Mapper):
add = rel_model.from_dict(value, delete) add = rel_model.from_dict(value, delete)
assert len(add) == 1 assert len(add) == 1
@ -247,24 +247,24 @@ class Base(db.Model):
# create item if necessary # create item if necessary
created = False created = False
item = model.query.get(data[pkey]) if pkey in data else None item = cls.query.get(data[pkey]) if pkey in data else None
if item is None: if item is None:
# check for mandatory keys # check for mandatory keys
missing = getattr(model, '_dict_mandatory', set()) - set(data.keys()) missing = getattr(cls, '_dict_mandatory', set()) - set(data.keys())
if missing: if missing:
raise ValueError(f'mandatory key(s) {", ".join(sorted(missing))} for {model.__table__} missing', model, missing, data) raise ValueError(f'mandatory key(s) {", ".join(sorted(missing))} for {cls.__table__} missing', cls, missing, data)
# remove mapped relationships from data # remove mapped relationships from data
mapped = {} mapped = {}
for key in list(data.keys()): for key in list(data.keys()):
if key in model.__mapper__.relationships: if key in cls.__mapper__.relationships:
if isinstance(model.__mapper__.relationships[key].argument, sqlalchemy.orm.Mapper): if isinstance(cls.__mapper__.relationships[key].argument, sqlalchemy.orm.Mapper):
mapped[key] = data[key] mapped[key] = data[key]
del data[key] del data[key]
# create new item # create new item
item = model(**data) item = cls(**data)
created = True created = True
# and update mapped relationships (below) # and update mapped relationships (below)
@ -278,14 +278,14 @@ class Base(db.Model):
if key == pkey: if key == pkey:
continue continue
if key in model.__mapper__.relationships: if key in cls.__mapper__.relationships:
# update relationship # update relationship
rel_model = model.__mapper__.relationships[key].argument rel_model = cls.__mapper__.relationships[key].argument
if isinstance(rel_model, sqlalchemy.orm.Mapper): if isinstance(rel_model, sqlalchemy.orm.Mapper):
rel_model = rel_model.class_ rel_model = rel_model.class_
# add (and create) referenced items # add (and create) referenced items
cur = getattr(item, key) cur = getattr(item, key)
old = sorted(cur, key=lambda i:id(i)) old = sorted(cur, key=id)
new = [] new = []
for rel_data in value: for rel_data in value:
# get or create related item # get or create related item
@ -331,16 +331,16 @@ class Base(db.Model):
break break
# remember changes # remember changes
new = sorted(new, key=lambda i:id(i)) new = sorted(new, key=id)
if new != old: if new != old:
updated.append((key, old, new)) updated.append((key, old, new))
else: else:
# update key # update key
old = getattr(item, key) old = getattr(item, key)
if type(old) is list: if isinstance(old, list):
# deduplicate list value # deduplicate list value
assert type(value) is list assert isinstance(value, list)
value = set(value) value = set(value)
old = set(old) old = set(old)
if not delete: if not delete:
@ -408,19 +408,19 @@ class Domain(Base):
if 'dkim_key' in data: if 'dkim_key' in data:
key = data['dkim_key'] key = data['dkim_key']
if key is not None: if key is not None:
if type(key) is list: if isinstance(key, list):
key = ''.join(key) key = ''.join(key)
if type(key) is str: if isinstance(key, str):
key = ''.join(key.strip().split()) # removes all whitespace key = ''.join(key.strip().split()) # removes all whitespace
if key == 'generate': if key == 'generate':
data['dkim_key'] = dkim.gen_key() data['dkim_key'] = dkim.gen_key()
elif key: elif key:
m = re.match('^-----BEGIN (RSA )?PRIVATE KEY-----', key) match = re.match('^-----BEGIN (RSA )?PRIVATE KEY-----', key)
if m is not None: if match is not None:
key = key[m.end():] key = key[match.end():]
m = re.search('-----END (RSA )?PRIVATE KEY-----$', key) match = re.search('-----END (RSA )?PRIVATE KEY-----$', key)
if m is not None: if match is not None:
key = key[:m.start()] key = key[:match.start()]
key = '\n'.join(wrap(key, 64)) key = '\n'.join(wrap(key, 64))
key = f'-----BEGIN PRIVATE KEY-----\n{key}\n-----END PRIVATE KEY-----\n'.encode('ascii') key = f'-----BEGIN PRIVATE KEY-----\n{key}\n-----END PRIVATE KEY-----\n'.encode('ascii')
try: try:
@ -428,7 +428,7 @@ class Domain(Base):
except: except:
raise ValueError('invalid dkim key') raise ValueError('invalid dkim key')
else: else:
data['dkim_key'] = key data['dkim_key'] = key
else: else:
data['dkim_key'] = None data['dkim_key'] = None
@ -505,8 +505,7 @@ class Domain(Base):
for email in self.users + self.aliases: for email in self.users + self.aliases:
if email.localpart == localpart: if email.localpart == localpart:
return True return True
else: return False
return False
def check_mx(self): def check_mx(self):
try: try:
@ -519,7 +518,7 @@ class Domain(Base):
return False return False
def __str__(self): def __str__(self):
return self.name return str(self.name)
def __eq__(self, other): def __eq__(self, other):
try: try:
@ -541,7 +540,7 @@ class Alternative(Base):
backref=db.backref('alternatives', cascade='all, delete-orphan')) backref=db.backref('alternatives', cascade='all, delete-orphan'))
def __str__(self): def __str__(self):
return self.name return str(self.name)
class Relay(Base): class Relay(Base):
@ -557,7 +556,7 @@ class Relay(Base):
smtp = db.Column(db.String(80), nullable=True) smtp = db.Column(db.String(80), nullable=True)
def __str__(self): def __str__(self):
return self.name return str(self.name)
class Email(object): class Email(object):
@ -571,7 +570,7 @@ class Email(object):
if 'email' in data: if 'email' in data:
if 'localpart' in data or 'domain' in data: if 'localpart' in data or 'domain' in data:
raise ValueError('ambigous key email and localpart/domain') raise ValueError('ambigous key email and localpart/domain')
elif type(data['email']) is str: elif isinstance(data['email'], str):
data['localpart'], data['domain'] = data['email'].rsplit('@', 1) data['localpart'], data['domain'] = data['email'].rsplit('@', 1)
else: else:
data['email'] = f'{data["localpart"]}@{data["domain"]}' data['email'] = f'{data["localpart"]}@{data["domain"]}'
@ -653,7 +652,7 @@ class Email(object):
return pure_alias.destination return pure_alias.destination
def __str__(self): def __str__(self):
return self.email return str(self.email)
class User(Base, Email): class User(Base, Email):
@ -750,7 +749,7 @@ class User(Base, Email):
'CRYPT': 'des_crypt'} 'CRYPT': 'des_crypt'}
def get_password_context(self): def get_password_context(self):
return context.CryptContext( return passlib.context.CryptContext(
schemes=self.scheme_dict.values(), schemes=self.scheme_dict.values(),
default=self.scheme_dict[app.config['PASSWORD_SCHEME']], default=self.scheme_dict[app.config['PASSWORD_SCHEME']],
) )
@ -818,7 +817,7 @@ class Alias(Base, Email):
Email._dict_input(data) Email._dict_input(data)
# handle comma delimited string for backwards compability # handle comma delimited string for backwards compability
dst = data.get('destination') dst = data.get('destination')
if type(dst) is str: if isinstance(dst, str):
data['destination'] = list([adr.strip() for adr in dst.split(',')]) data['destination'] = list([adr.strip() for adr in dst.split(',')])
domain = db.relationship(Domain, domain = db.relationship(Domain,
@ -888,10 +887,10 @@ class Token(Base):
ip = db.Column(db.String(255)) ip = db.Column(db.String(255))
def check_password(self, password): def check_password(self, password):
return hash.sha256_crypt.verify(password, self.password) return passlib.hash.sha256_crypt.verify(password, self.password)
def set_password(self, password): def set_password(self, password):
self.password = hash.sha256_crypt.using(rounds=1000).hash(password) self.password = passlib.hash.sha256_crypt.using(rounds=1000).hash(password)
def __str__(self): def __str__(self):
return self.comment or self.ip return self.comment or self.ip

Loading…
Cancel
Save