From 4c258f5a6b8ba082f016cf186884b42e8a3d8549 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Wed, 6 Jan 2021 16:45:55 +0100 Subject: [PATCH] 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) --- core/admin/mailu/manage.py | 33 ++++++------ core/admin/mailu/models.py | 107 ++++++++++++++++++------------------- 2 files changed, 69 insertions(+), 71 deletions(-) diff --git a/core/admin/mailu/manage.py b/core/admin/mailu/manage.py index f70c5c85..c07ca2b7 100644 --- a/core/admin/mailu/manage.py +++ b/core/admin/mailu/manage.py @@ -1,9 +1,8 @@ from mailu import models 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 socket import uuid @@ -15,14 +14,14 @@ import sys db = models.db -@click.group() -def mailu(cls=flask_cli.FlaskGroup): +@click.group(cls=FlaskGroup) +def mailu(): """ Mailu command line """ @mailu.command() -@flask_cli.with_appcontext +@with_appcontext def advertise(): """ Advertise this server against statistic services. """ @@ -45,7 +44,7 @@ def advertise(): @click.argument('domain_name') @click.argument('password') @click.option('-m', '--mode') -@flask_cli.with_appcontext +@with_appcontext def admin(localpart, domain_name, password, mode='create'): """ Create an admin user 'mode' can be: @@ -89,7 +88,7 @@ def admin(localpart, domain_name, password, mode='create'): @click.argument('domain_name') @click.argument('password') @click.argument('hash_scheme', required=False) -@flask_cli.with_appcontext +@with_appcontext def user(localpart, domain_name, password, hash_scheme=None): """ Create a user """ @@ -114,7 +113,7 @@ def user(localpart, domain_name, password, hash_scheme=None): @click.argument('domain_name') @click.argument('password') @click.argument('hash_scheme', required=False) -@flask_cli.with_appcontext +@with_appcontext def password(localpart, domain_name, password, hash_scheme=None): """ 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('-a', '--max-aliases') @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): """ 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('password_hash') @click.argument('hash_scheme') -@flask_cli.with_appcontext +@with_appcontext def user_import(localpart, domain_name, password_hash, hash_scheme = None): """ 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('-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') -@flask_cli.with_appcontext +@with_appcontext def config_update(verbose=False, delete_objects=False, dry_run=False, file=None): """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('-d', '--dns', is_flag=True, help='Include dns records') @click.argument('sections', nargs=-1) -@flask_cli.with_appcontext +@with_appcontext def config_dump(full=False, secrets=False, dns=False, sections=None): """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() @click.argument('email') -@flask_cli.with_appcontext +@with_appcontext def user_delete(email): """delete user""" user = models.User.query.get(email) @@ -354,7 +353,7 @@ def user_delete(email): @mailu.command() @click.argument('email') -@flask_cli.with_appcontext +@with_appcontext def alias_delete(email): """delete alias""" alias = models.Alias.query.get(email) @@ -368,7 +367,7 @@ def alias_delete(email): @click.argument('domain_name') @click.argument('destination') @click.option('-w', '--wildcard', is_flag=True) -@flask_cli.with_appcontext +@with_appcontext def alias(localpart, domain_name, destination, wildcard=False): """ Create an alias """ @@ -392,7 +391,7 @@ def alias(localpart, domain_name, destination, wildcard=False): @click.argument('max_users') @click.argument('max_aliases') @click.argument('max_quota_bytes') -@flask_cli.with_appcontext +@with_appcontext def setlimits(domain_name, max_users, max_aliases, max_quota_bytes): """ Set domain limits """ @@ -407,7 +406,7 @@ def setlimits(domain_name, max_users, max_aliases, max_quota_bytes): @mailu.command() @click.argument('domain_name') @click.argument('user_name') -@flask_cli.with_appcontext +@with_appcontext def setmanager(domain_name, user_name='manager'): """ Make a user manager of a domain """ diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 13ebce60..3bf92244 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -1,7 +1,6 @@ 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 @@ -12,6 +11,7 @@ import sqlalchemy import re import time import os +import passlib import glob import smtplib import idna @@ -113,8 +113,8 @@ class Base(db.Model): comment = db.Column(db.String(255), nullable=True) @classmethod - def _dict_pkey(model): - return model.__mapper__.primary_key[0].name + def _dict_pkey(cls): + return cls.__mapper__.primary_key[0].name def _dict_pval(self): return getattr(self, self._dict_pkey()) @@ -187,57 +187,57 @@ class Base(db.Model): return res @classmethod - def from_dict(model, data, delete=False): + def from_dict(cls, data, delete=False): changed = [] - pkey = model._dict_pkey() + pkey = cls._dict_pkey() # handle "primary key" only - if type(data) is not dict: + if isinstance(data, dict): data = {pkey: data} # modify input data - if hasattr(model, '_dict_input'): + if hasattr(cls, '_dict_input'): try: - model._dict_input(data) + cls._dict_input(data) 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) - if not getattr(model, '_dict_recurse', False): + if not getattr(cls, '_dict_recurse', False): 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 for key in list(data.keys()): # check key - if not hasattr(model, key) and not key in model.__mapper__.relationships: - raise KeyError(f'unknown key {model.__table__}.{key}', model, key, data) + if not hasattr(cls, key) and not key in cls.__mapper__.relationships: + raise KeyError(f'unknown key {cls.__table__}.{key}', cls, key, data) # check value type value = data[key] - col = model.__mapper__.columns.get(key) + col = cls.__mapper__.columns.get(key) if col is not None: - if not ((value is None and col.nullable) or (type(value) is col.type.python_type)): - raise TypeError(f'{model.__table__}.{key} {value!r} has invalid type {type(value).__name__!r}', model, key, data) + if not ((value is None and col.nullable) or (isinstance(value, col.type.python_type))): + raise TypeError(f'{cls.__table__}.{key} {value!r} has invalid type {type(value).__name__!r}', cls, key, data) else: - rel = model.__mapper__.relationships.get(key) + rel = cls.__mapper__.relationships.get(key) 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 False: # ignore value. TODO: emit warning? del data[key] continue 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: - raise NotImplementedError(f'type not defined for {model.__table__}.{key}') + raise NotImplementedError(f'type not defined for {cls.__table__}.{key}') # handle relationships - if key in model.__mapper__.relationships: - rel_model = model.__mapper__.relationships[key].argument + if key in cls.__mapper__.relationships: + rel_model = cls.__mapper__.relationships[key].argument if not isinstance(rel_model, sqlalchemy.orm.Mapper): add = rel_model.from_dict(value, delete) assert len(add) == 1 @@ -247,24 +247,24 @@ class Base(db.Model): # create item if necessary 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: # check for mandatory keys - missing = getattr(model, '_dict_mandatory', set()) - set(data.keys()) + missing = getattr(cls, '_dict_mandatory', set()) - set(data.keys()) 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 mapped = {} for key in list(data.keys()): - if key in model.__mapper__.relationships: - if isinstance(model.__mapper__.relationships[key].argument, sqlalchemy.orm.Mapper): + if key in cls.__mapper__.relationships: + if isinstance(cls.__mapper__.relationships[key].argument, sqlalchemy.orm.Mapper): mapped[key] = data[key] del data[key] # create new item - item = model(**data) + item = cls(**data) created = True # and update mapped relationships (below) @@ -278,14 +278,14 @@ class Base(db.Model): if key == pkey: continue - if key in model.__mapper__.relationships: + if key in cls.__mapper__.relationships: # update relationship - rel_model = model.__mapper__.relationships[key].argument + rel_model = cls.__mapper__.relationships[key].argument if isinstance(rel_model, sqlalchemy.orm.Mapper): rel_model = rel_model.class_ # add (and create) referenced items cur = getattr(item, key) - old = sorted(cur, key=lambda i:id(i)) + old = sorted(cur, key=id) new = [] for rel_data in value: # get or create related item @@ -331,16 +331,16 @@ class Base(db.Model): break # remember changes - new = sorted(new, key=lambda i:id(i)) + new = sorted(new, key=id) if new != old: updated.append((key, old, new)) else: # update key old = getattr(item, key) - if type(old) is list: + if isinstance(old, list): # deduplicate list value - assert type(value) is list + assert isinstance(value, list) value = set(value) old = set(old) if not delete: @@ -408,19 +408,19 @@ class Domain(Base): if 'dkim_key' in data: key = data['dkim_key'] if key is not None: - if type(key) is list: + if isinstance(key, list): key = ''.join(key) - if type(key) is str: + if isinstance(key, str): key = ''.join(key.strip().split()) # removes all whitespace if key == 'generate': data['dkim_key'] = dkim.gen_key() elif key: - m = re.match('^-----BEGIN (RSA )?PRIVATE KEY-----', key) - if m is not None: - key = key[m.end():] - m = re.search('-----END (RSA )?PRIVATE KEY-----$', key) - if m is not None: - key = key[:m.start()] + match = re.match('^-----BEGIN (RSA )?PRIVATE KEY-----', key) + if match is not None: + key = key[match.end():] + match = re.search('-----END (RSA )?PRIVATE KEY-----$', key) + if match is not None: + key = key[:match.start()] key = '\n'.join(wrap(key, 64)) key = f'-----BEGIN PRIVATE KEY-----\n{key}\n-----END PRIVATE KEY-----\n'.encode('ascii') try: @@ -428,7 +428,7 @@ class Domain(Base): except: raise ValueError('invalid dkim key') else: - data['dkim_key'] = key + data['dkim_key'] = key else: data['dkim_key'] = None @@ -505,8 +505,7 @@ class Domain(Base): for email in self.users + self.aliases: if email.localpart == localpart: return True - else: - return False + return False def check_mx(self): try: @@ -519,7 +518,7 @@ class Domain(Base): return False def __str__(self): - return self.name + return str(self.name) def __eq__(self, other): try: @@ -541,7 +540,7 @@ class Alternative(Base): backref=db.backref('alternatives', cascade='all, delete-orphan')) def __str__(self): - return self.name + return str(self.name) class Relay(Base): @@ -557,7 +556,7 @@ class Relay(Base): smtp = db.Column(db.String(80), nullable=True) def __str__(self): - return self.name + return str(self.name) class Email(object): @@ -571,7 +570,7 @@ class Email(object): if 'email' in data: if 'localpart' in data or 'domain' in data: 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) else: data['email'] = f'{data["localpart"]}@{data["domain"]}' @@ -653,7 +652,7 @@ class Email(object): return pure_alias.destination def __str__(self): - return self.email + return str(self.email) class User(Base, Email): @@ -750,7 +749,7 @@ class User(Base, Email): 'CRYPT': 'des_crypt'} def get_password_context(self): - return context.CryptContext( + return passlib.context.CryptContext( schemes=self.scheme_dict.values(), default=self.scheme_dict[app.config['PASSWORD_SCHEME']], ) @@ -818,7 +817,7 @@ class Alias(Base, Email): Email._dict_input(data) # handle comma delimited string for backwards compability dst = data.get('destination') - if type(dst) is str: + if isinstance(dst, str): data['destination'] = list([adr.strip() for adr in dst.split(',')]) domain = db.relationship(Domain, @@ -888,10 +887,10 @@ class Token(Base): ip = db.Column(db.String(255)) 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): - self.password = hash.sha256_crypt.using(rounds=1000).hash(password) + self.password = passlib.hash.sha256_crypt.using(rounds=1000).hash(password) def __str__(self): return self.comment or self.ip