@ -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 )
m atch = re . match ( ' ^-----BEGIN (RSA )?PRIVATE KEY----- ' , key )
if m is not None :
if m atch is not None :
key = key [ m . end ( ) : ]
key = key [ m atch . end ( ) : ]
m = re . search ( ' -----END (RSA )?PRIVATE KEY-----$ ' , key )
m atch = re . search ( ' -----END (RSA )?PRIVATE KEY-----$ ' , key )
if m is not None :
if m atch is not None :
key = key [ : m . start ( ) ]
key = key [ : m atch . 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