mirror of
https://github.com/optim-enterprises-bv/Mailu.git
synced 2025-11-02 02:57:56 +00:00
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)
This commit is contained in:
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user