Add files via upload

This commit is contained in:
Harold Finch
2023-04-10 07:18:32 +02:00
committed by GitHub
parent 06ddbf431f
commit 65875d8fef
100 changed files with 84692 additions and 42 deletions

66
libs/sqllib/__init__.py Normal file
View File

@@ -0,0 +1,66 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs.logger import logger
class MYSQLWrap:
def __del__(self):
try:
self.conn.ctx.db.close()
except Exception:
pass
def connect(self):
conn = web.database(
dbn='mysql',
host=settings.vmail_db_host,
port=int(settings.vmail_db_port),
db=settings.vmail_db_name,
user=settings.vmail_db_user,
pw=settings.vmail_db_password,
charset='utf8')
conn.supports_multiple_insert = True
return conn
def __init__(self):
try:
self.conn = self.connect()
except AttributeError:
# Reconnect if error raised: MySQL server has gone away.
self.conn = self.connect()
except Exception as e:
logger.error(e)
class PGSQLWrap:
def __del__(self):
try:
self.conn.ctx.db.close()
except Exception:
pass
def __init__(self):
# Initial DB connection and cursor.
try:
self.conn = web.database(
dbn='postgres',
host=settings.vmail_db_host,
port=int(settings.vmail_db_port),
db=settings.vmail_db_name,
user=settings.vmail_db_user,
pw=settings.vmail_db_password,
)
self.conn.supports_multiple_insert = True
except Exception as e:
logger.error(e)
if settings.backend == 'mysql':
SQLWrap = MYSQLWrap
elif settings.backend == 'pgsql':
SQLWrap = PGSQLWrap

1279
libs/sqllib/admin.py Normal file

File diff suppressed because it is too large Load Diff

656
libs/sqllib/alias.py Normal file
View File

@@ -0,0 +1,656 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs import iredutils, form_utils
from libs.logger import logger, log_activity
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import general as sql_lib_general
from libs.sqllib import domain as sql_lib_domain
session = web.config.get('_session')
@decorators.require_domain_access
def change_email(mail, new_mail, conn=None):
if not iredutils.is_email(mail):
return False, 'INVALID_OLD_EMAIL'
if not iredutils.is_email(new_mail):
return False, 'INVALID_NEW_EMAIL'
old_domain = mail.split('@', 1)[-1]
new_domain = new_mail.split('@', 1)[-1]
if old_domain != new_domain:
return False, 'PERMISSION_DENIED'
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
if not sql_lib_general.is_email_exists(mail=mail, conn=conn):
return False, 'OLD_EMAIL_NOT_EXIST'
if sql_lib_general.is_email_exists(mail=new_mail, conn=conn):
return False, 'NEW_EMAIL_ALREADY_EXISTS'
# Change email address
try:
sql_vars = {'mail': mail, 'new_mail': new_mail}
conn.update('alias',
vars=sql_vars,
address=new_mail,
where='address=$mail')
# Update per-user mail forwardings, alias memberships
conn.update('forwardings',
vars=sql_vars,
address=new_mail,
where='address=$mail')
conn.update('forwardings',
vars=sql_vars,
forwarding=new_mail,
where='forwarding=$mail')
# Update moderators
conn.update('moderators',
vars=sql_vars,
address=new_mail,
where='address=$mail')
conn.update('moderators',
vars=sql_vars,
moderator=new_mail,
where='moderator=$mail')
log_activity(event='update',
domain=old_domain,
msg="Change alias account email address: {} -> {}.".format(mail, new_mail))
return True,
except Exception as e:
return False, repr(e)
def add_alias_from_form(domain, form, conn=None):
# Get domain name, username, cn.
form_domain = form_utils.get_domain_name(form)
username = web.safestr(form.get('listname')).strip().lower()
mail = username + '@' + form_domain
if domain != form_domain:
return False, 'PERMISSION_DENIED'
if not iredutils.is_domain(domain):
return False, 'INVALID_DOMAIN_NAME'
if not iredutils.is_auth_email(mail):
return False, 'INVALID_MAIL'
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
# Check account existing.
if sql_lib_general.is_email_exists(mail=mail, conn=conn):
return False, 'ALREADY_EXISTS'
# Get domain profile.
qr_profile = sql_lib_domain.profile(conn=conn, domain=domain)
if qr_profile[0]:
domain_profile = qr_profile[1]
else:
return qr_profile
# Check account limit.
num_exist = num_aliases_under_domain(conn=conn, domain=domain)
if domain_profile.aliases == -1:
return False, 'NOT_ALLOWED'
elif domain_profile.aliases > 0:
if domain_profile.aliases <= num_exist:
return False, 'EXCEEDED_DOMAIN_ACCOUNT_LIMIT'
# Define columns and values used to insert.
columns = {
'address': mail, 'domain': domain,
'name': form_utils.get_name(form=form),
'created': iredutils.get_gmttime(), 'active': 1,
'accesspolicy': form_utils.get_list_access_policy(form=form,
input_name='accessPolicy',
default_value='public'),
}
# Get access policy
try:
conn.insert('alias', **columns)
log_activity(msg="Create mail alias: %s." % mail,
domain=domain,
event='create')
return True,
except Exception as e:
return False, repr(e)
def delete_aliases(accounts, conn=None):
"""Delete alias accounts under same domain."""
accounts = [str(i).lower() for i in accounts if iredutils.is_email(i)]
if not accounts:
return True,
# Get domain from first account
domain = accounts[0].split('@', 1)[-1]
if not iredutils.is_domain(domain):
return True,
sql_vars = {'domain': domain, 'accounts': accounts}
try:
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
conn.delete('alias',
vars=sql_vars,
where='address IN $accounts')
conn.delete('forwardings',
vars=sql_vars,
where='address IN $accounts OR forwarding IN $accounts')
log_activity(event='delete',
domain=accounts[0].split('@', 1)[-1],
msg="Delete alias: %s." % ', '.join(accounts))
except Exception as e:
return False, repr(e)
# Remove alias from domain.settings: default_groups
qr = sql_lib_domain.remove_default_maillists_in_domain_setting(domain=domain,
maillists=accounts,
conn=conn)
if not qr[0]:
return qr
return True,
@decorators.require_domain_access
def num_aliases_under_domain(conn, domain, disabled_only=False, first_char=None):
if not iredutils.is_domain(domain):
return False, 'INVALID_DOMAIN_NAME'
num = 0
sql_vars = {'domain': domain}
sql_where = ''
if disabled_only:
sql_where = ' AND active=0'
if first_char:
sql_where += ' AND address LIKE %s' % web.sqlquote(first_char.lower() + '%')
try:
qr = conn.select('alias',
vars=sql_vars,
what='COUNT(address) AS total',
where='domain=$domain %s' % sql_where)
num = qr[0].total or 0
except:
pass
return num
@decorators.require_domain_access
def get_basic_alias_profiles(domain,
columns=None,
first_char=None,
page=0,
email_only=False,
disabled_only=False,
conn=None):
"""Get all aliases under domain.
Return data:
(True, [{'mail': 'alias@domain.com',
'name': '...',
...other profiles in `vmail.alias` table...
'members', [...],
'moderators', [...]]
"""
domain = web.safestr(domain).lower()
if not iredutils.is_domain(domain):
raise web.seeother('/domains?msg=INVALID_DOMAIN_NAME')
sql_vars = {'domain': domain}
if columns:
sql_what = ','.join(columns)
else:
if email_only:
sql_what = 'address'
else:
sql_what = '*'
# Get alias members
additional_sql_where = ''
if first_char:
additional_sql_where = ' AND address LIKE %s' % web.sqlquote(first_char.lower() + '%')
if disabled_only:
additional_sql_where = ' AND active=0'
# Get basic alias profiles first
try:
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
if page:
qr = conn.select('alias',
vars=sql_vars,
what=sql_what,
where='domain=$domain %s' % additional_sql_where,
order='address ASC',
limit=settings.PAGE_SIZE_LIMIT,
offset=(page - 1) * settings.PAGE_SIZE_LIMIT)
else:
qr = conn.select('alias',
vars=sql_vars,
what=sql_what,
where='domain=$domain %s' % additional_sql_where,
order='address ASC')
if email_only:
emails = []
for r in qr:
email = str(r.address).lower()
emails.append(email)
emails.sort()
return True, emails
else:
return True, list(qr)
except Exception as e:
return False, repr(e)
@decorators.require_domain_access
def get_profile(mail,
with_members=True,
with_moderators=True,
conn=None):
if not iredutils.is_email(mail):
return False, 'INVALID_MAIL'
try:
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
qr = conn.select('alias',
vars={'address': mail},
where='address=$address',
limit=1)
if qr:
profile = list(qr)[0]
if with_members:
_qr = get_member_emails(mail=mail, conn=conn)
if _qr[0]:
profile['members'] = _qr[1]
profile['members'].sort()
else:
return _qr
if with_moderators:
_qr = get_moderators(mail=mail, conn=conn)
if _qr[0]:
profile['moderators'] = _qr[1]
profile['moderators'].sort()
else:
return _qr
return True, profile
else:
return False, 'NO_SUCH_ACCOUNT'
except Exception as e:
return False, repr(e)
@decorators.require_domain_access
def update(mail, profile_type, form, conn=None):
mail = web.safestr(mail).lower()
domain = mail.split('@', 1)[-1]
if not iredutils.is_email(mail):
return False, 'INVALID_MAIL'
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
# change email address
if profile_type == 'rename':
# new email address
new_mail = web.safestr(form.get('new_mail_username')).strip().lower() + '@' + domain
qr = change_email(mail=mail, new_mail=new_mail, conn=conn)
if qr[0]:
raise web.seeother('/profile/alias/general/%s?msg=EMAIL_CHANGED' % new_mail)
else:
raise web.seeother('/profile/alias/general/{}?msg={}'.format(new_mail, web.urlquote(qr[1])))
# Pre-defined.
values = {'modified': iredutils.get_gmttime()}
# Get cn.
cn = form.get('cn', '')
values['name'] = cn
# check account status.
values['active'] = 0
if 'accountStatus' in form:
# Enabled.
values['active'] = 1
# Get access policy.
access_policy = str(form.get('accessPolicy'))
if access_policy in iredutils.MAILLIST_ACCESS_POLICIES:
values['accesspolicy'] = access_policy
# Get members & moderators from web form.
_members = form_utils.get_multi_values_from_textarea(form=form,
input_name='members',
is_email=True)
_members = list({iredutils.lower_email_with_upper_ext_address(v) for v in _members})
_moderators = [str(v).strip().lower() for v in form.get('moderators', '').splitlines()]
_moderators = list({iredutils.lower_email_with_upper_ext_address(v)
for v in _moderators
if iredutils.is_email(v) or v.startswith('*@')})
_moderators_wildcard = [v for v in _moderators if iredutils.is_domain(v.split('@', 1)[-1])]
# Remove non-exist accounts in same domain.
# Get members & moderators which in same domain.
_members_in_domain = [i for i in _members if i.endswith('@' + domain)]
_members_not_in_domain = [i for i in _members if not i.endswith('@' + domain)]
_moderators_in_domain = [i for i in _moderators if i.endswith('@' + domain) and i not in _moderators_wildcard]
_moderators_not_in_domain = [i for i in _moderators if not (i.endswith('@' + domain) or i in _moderators_wildcard)]
# Verify internal users
addresses_in_domain = []
_addresses_in_domain = list(set(_members_in_domain + _moderators_in_domain))
if _addresses_in_domain:
try:
# Remove non-existing addresses
_qr = sql_lib_general.filter_existing_emails(mails=_addresses_in_domain, conn=conn)
addresses_in_domain = _qr['exist']
except Exception as e:
logger.error(e)
members_in_domain = [v for v in _members_in_domain if v in addresses_in_domain]
moderators_in_domain = [v for v in _moderators_in_domain if v in addresses_in_domain]
try:
# Update profile
conn.update('alias',
vars={'address': mail},
where='address=$address',
**values)
# Delete all members and moderators first
conn.delete('forwardings',
vars={'address': mail},
where='address=$address')
conn.delete('moderators',
vars={'address': mail},
where='address=$address')
# Add members by inserting new records
_all_members = members_in_domain + _members_not_in_domain
if _all_members:
v = []
for _member in _all_members:
v += [{'address': mail,
'forwarding': _member,
'domain': domain,
'dest_domain': _member.split('@', 1)[-1],
'active': values['active'],
'is_list': 1}]
conn.multiple_insert('forwardings', values=v)
# Add moderators by inserting new records
_all_moderators = moderators_in_domain + _moderators_not_in_domain + _moderators_wildcard
if _all_moderators:
v = []
for _moderator in _all_moderators:
v += [{'address': mail,
'moderator': _moderator,
'domain': domain,
'dest_domain': _moderator.split('@', 1)[-1]}]
conn.multiple_insert('moderators', values=v)
# Log changes.
msg = "Update alias profile (%s)." % mail
if access_policy:
msg += " Access policy: %s." % access_policy
if _all_members:
msg += " Members: %s." % (', '.join(_all_members))
else:
msg += " No members."
if _all_moderators:
msg += " Moderators: %s." % (', '.join(_all_moderators))
else:
msg += " No moderators."
log_activity(msg=msg, username=mail, domain=domain, event='update')
return True,
except Exception as e:
return False, repr(e)
def get_member_emails(mail, conn=None):
"""Get members of mail alias account. Return a list of mail addresses.
Return a list with all members' email addresses."""
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
try:
qr = conn.select(
'forwardings',
vars={'mail': mail},
what='forwarding',
where='address=$mail AND is_list=1',
)
_addresses = [iredutils.lower_email_with_upper_ext_address(i.forwarding)
for i in qr if iredutils.is_email(i.forwarding)]
_addresses.sort()
return True, _addresses
except Exception as e:
return False, repr(e)
def get_moderators(mail, conn=None):
"""Get moderators of given mail alias account.
Return a list with all moderators' email addresses."""
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
try:
qr = conn.select('moderators',
vars={'mail': mail},
what='moderator',
where='address=$mail')
_addresses = [iredutils.lower_email_with_upper_ext_address(i.moderator)
for i in qr
if iredutils.is_email(i.moderator) or i.moderator.startswith('*@')]
_addresses.sort()
return True, _addresses
except Exception as e:
return False, repr(e)
def reset_members(mail, members, conn=None):
"""Assign all given addresses specified in `@members` as members."""
_addresses = {iredutils.lower_email_with_upper_ext_address(i)
for i in members
if iredutils.is_email(i)}
domain = mail.split('@', 1)[-1]
_addresses_in_domain = [v for v in _addresses if v.endswith('@' + domain) and v != mail]
_addresses_not_in_domain = [v for v in _addresses if not v.endswith('@' + domain)]
del _addresses
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
# Verify existence of addresses in same domain
if _addresses_in_domain:
try:
# Remove non-existing addresses
qr = sql_lib_general.filter_existing_emails(mails=_addresses_in_domain, conn=conn)
_addresses_in_domain = qr['exist']
except Exception as e:
logger.error(e)
try:
# Delete all existing members first
conn.delete('forwardings',
vars={'mail': mail},
where='address=$mail AND is_list=1')
# Add member by inserting new record
_all_addresses = _addresses_in_domain + _addresses_not_in_domain
if _all_addresses:
v = []
for i in _all_addresses:
v += [{'address': mail,
'forwarding': i,
'domain': domain,
'dest_domain': i.split('@', 1)[-1],
'is_list': 1}]
conn.multiple_insert('forwardings', values=v)
log_activity(msg='Reset alias ({}) members to: {}'.format(mail, ', '.join(_all_addresses)),
admin=session.get('username'),
username=mail,
domain=domain,
event='update')
return True,
except Exception as e:
return False, repr(e)
def update_members(mail,
new_members=None,
removed_members=None,
conn=None):
"""Add new members to mail alias account, and remove removed_members."""
_new = []
if new_members:
_new = [iredutils.lower_email_with_upper_ext_address(i)
for i in new_members if iredutils.is_email(i)]
_removed = []
if removed_members:
_removed = [iredutils.lower_email_with_upper_ext_address(i)
for i in removed_members if iredutils.is_email(i)]
if not (_new or _removed):
return True, 'NO_VALID_MEMBERS'
domain = mail.split('@', 1)[-1]
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
# Verify existence of addresses in same domain
_new_in_domain = set()
_new_not_in_domain = set()
if _new:
for i in _new:
if i.endswith('@' + domain):
_new_in_domain.add(i)
else:
_new_not_in_domain.add(i)
# remove self
_new_in_domain.discard(mail)
if _new_in_domain:
try:
# Remove non-existing addresses
qr = sql_lib_general.filter_existing_emails(mails=_new_in_domain, conn=conn)
_new_in_domain = qr['exist']
except Exception as e:
logger.error(e)
# Get existing members
qr = get_member_emails(mail=mail, conn=conn)
if qr[0]:
_old_members = qr[1]
else:
return qr
# Add new, remove removed
_members = set(_old_members)
_members.update(_new_in_domain)
_members.update(_new_not_in_domain)
_members -= set(_removed)
try:
# Delete all existing members first
conn.delete('forwardings',
vars={'mail': mail},
where='address=$mail AND is_list=1')
# Add member by inserting new record
if _members:
v = []
for i in _members:
v += [{'address': mail,
'forwarding': i,
'domain': domain,
'dest_domain': i.split('@', 1)[-1],
'is_list': 1}]
conn.multiple_insert('forwardings', values=v)
log_activity(msg='Update alias ({}) members to: {}'.format(mail, ', '.join(_members)),
admin=session.get('username'),
username=mail,
domain=domain,
event='update')
return True,
except Exception as e:
return False, repr(e)

73
libs/sqllib/api_utils.py Normal file
View File

@@ -0,0 +1,73 @@
import datetime
from libs import form_utils
from libs.sqllib import general as sql_lib_general
from libs.sqllib import sqlutils
import settings
def get_form_password_dict(form,
domain,
input_name='password',
min_passwd_length=None,
max_passwd_length=None):
"""Extract password from form, verify it, return both plain and hashed password.
>>> get_form_password_dict(form=form,
domain='domain.tld',
input_name='password',
min_passwd_length=None,
max_passwd_length=None)
(True, {'pw_plain', '123456',
'pw_hash', '{SSHA512}....'})
"""
if input_name not in form:
return False, 'NO_PASSWORD'
# Get min/max password length from domain profile
if not (min_passwd_length or max_passwd_length):
qr = sql_lib_general.get_domain_settings(domain=domain)
if qr[0]:
ds = qr[1]
min_passwd_length = ds.get('min_passwd_length', settings.min_passwd_length)
max_passwd_length = ds.get('max_passwd_length', settings.max_passwd_length)
qr = form_utils.get_password(form=form,
input_name=input_name,
confirm_pw_input_name=input_name,
min_passwd_length=min_passwd_length,
max_passwd_length=max_passwd_length)
return qr
def export_sql_record(record, remove_columns=None):
"""Convert some values in SQL format to general string.
- datetime
- settings
"""
for (k, v) in list(record.items()):
if remove_columns:
if k in remove_columns:
record.pop(k)
continue
if isinstance(v, datetime.datetime):
record[k] = v.isoformat()
elif k == 'settings':
record[k] = sqlutils.account_settings_string_to_dict(v)
return record
def export_sql_records(records, remove_columns=None):
new_records = []
for rcd in records:
new_rcd = export_sql_record(record=rcd, remove_columns=remove_columns)
new_records.append(new_rcd)
return new_records

138
libs/sqllib/auth.py Normal file
View File

@@ -0,0 +1,138 @@
import web
import settings
from libs import iredutils, iredpwd
from libs.l10n import TIMEZONES
from libs.sqllib import sqlutils
session = web.config.get('_session', {})
def auth(conn,
username,
password,
account_type='admin',
verify_password=False):
if not iredutils.is_email(username):
return False, 'INVALID_USERNAME'
if not password:
return False, 'EMPTY_PASSWORD'
username = str(username).lower()
password = str(password)
domain = username.split('@', 1)[-1]
# Query account from SQL database.
if account_type == 'admin':
# separate admin accounts
result = conn.select('admin',
vars={'username': username},
where="username=$username AND active=1",
what='password, language, settings',
limit=1)
# mail user marked as domain admin
if not result:
result = conn.select(
["mailbox", "domain"],
vars={'username': username},
where="mailbox.username=$username AND mailbox.active=1 AND (mailbox.isadmin=1 OR mailbox.isglobaladmin=1) AND mailbox.domain=domain.domain and domain.active=1",
what='mailbox.password, mailbox.language, mailbox.isadmin, mailbox.isglobaladmin, mailbox.settings',
limit=1,
)
if result:
session['admin_is_mail_user'] = True
elif account_type == 'user':
result = conn.select('mailbox',
vars={'username': username},
what='password, language, isadmin, isglobaladmin, settings',
where="username=$username AND active=1",
limit=1)
else:
return False, 'INVALID_ACCOUNT_TYPE'
if not result:
# Account not found.
# Do NOT return msg like 'Account does not ***EXIST***', crackers
# can use it to verify valid accounts.
return False, 'INVALID_CREDENTIALS'
record = result[0]
password_sql = str(record.password)
account_settings = sqlutils.account_settings_string_to_dict(str(record.settings))
# Verify password
if not iredpwd.verify_password_hash(password_sql, password):
return False, 'INVALID_CREDENTIALS'
if not verify_password:
session['username'] = username
if account_type == 'user':
session['account_is_mail_user'] = True
# Set preferred language.
session['lang'] = web.safestr(record.get('language', settings.default_language))
# Set timezone (GMT-XX:XX).
# Priority: per-user timezone > per-domain > global setting
timezone = settings.LOCAL_TIMEZONE
if 'timezone' in account_settings:
tz_name = account_settings['timezone']
if tz_name in TIMEZONES:
timezone = TIMEZONES[tz_name]
else:
# Get per-domain timezone
qr_domain = conn.select('domain',
vars={'domain': domain},
what='settings',
where='domain=$domain',
limit=1)
if qr_domain:
domain_settings = sqlutils.account_settings_string_to_dict(str(qr_domain[0]['settings']))
if 'timezone' in domain_settings:
tz_name = domain_settings['timezone']
if tz_name in TIMEZONES:
timezone = TIMEZONES[tz_name]
session['timezone'] = timezone
# Set session['is_global_admin']
if session.get('admin_is_mail_user'):
if record.get('isglobaladmin', 0) == 1:
session['is_global_admin'] = True
else:
session['is_normal_admin'] = True
# Set session['allowed_to_grant_admin']
if 'grant_admin' in account_settings:
session['allowed_to_grant_admin'] = True
else:
try:
result = conn.select('domain_admins',
vars={'username': username, 'domain': 'ALL'},
what='domain',
where='username=$username AND domain=$domain',
limit=1)
if result:
session['is_global_admin'] = True
else:
if account_type == 'admin':
session['is_normal_admin'] = True
except:
pass
if session['is_global_admin']:
if not iredutils.is_allowed_global_admin_login_ip(client_ip=web.ctx.ip):
session.kill()
raise web.seeother('/login?msg=NOT_ALLOWED_IP')
session['logged'] = True
web.config.session_parameters['cookie_name'] = 'iRedAdmin-Pro'
web.config.session_parameters['ignore_change_ip'] = settings.SESSION_IGNORE_CHANGE_IP
web.config.session_parameters['ignore_expiry'] = False
return True, {'account_settings': account_settings}

201
libs/sqllib/decorators.py Normal file
View File

@@ -0,0 +1,201 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from controllers import decorators as base_decorators
from controllers.utils import api_render
from libs import iredutils
from libs.logger import logger
from libs.sqllib import general as sql_lib_general
session = web.config.get('_session', {})
# require_api_auth_token = base_decorators.require_api_auth_token
require_login = base_decorators.require_login
require_admin_login = base_decorators.require_admin_login
require_global_admin = base_decorators.require_global_admin
csrf_protected = base_decorators.csrf_protected
require_permission_create_domain = base_decorators.require_permission_create_domain
require_preference_access = base_decorators.require_preference_access
api_require_admin_login = base_decorators.api_require_admin_login
api_require_global_admin = base_decorators.api_require_global_admin
def require_domain_access(func):
def proxyfunc(*args, **kw):
if not session.get('username'):
raise web.seeother('/login?msg=LOGIN_REQUIRED')
# Check domain global admin.
if session.get('is_global_admin'):
return func(*args, **kw)
else:
username = session.get('username')
# admin/user is viewing its own data
if username == kw.get('mail') \
or username.endswith('@' + kw.get('domain', 'NONE')):
return func(*args, **kw)
if 'domain' in kw and iredutils.is_domain(kw.get('domain')):
domain = web.safestr(kw['domain'])
elif 'mail' in kw and iredutils.is_email(kw.get('mail')):
domain = web.safestr(kw['mail']).split('@')[-1]
elif 'admin' in kw and iredutils.is_email(kw.get('admin')):
domain = web.safestr(kw['admin']).split('@')[-1]
else:
domain = None
# Try to use the first valid domain name or email address as
# key, it's passed from controllers/*.
for arg in args:
if iredutils.is_domain(arg):
domain = arg
break
elif iredutils.is_email(arg):
domain = arg.split('@', 1)[-1]
break
if not domain:
if settings.LOG_PERMISSION_DENIED:
logger.error("PERMISSION_DENIED (1) raised in "
"@require_domain_access, triggered in module: "
"%s.py, function: %s(). No target domain for "
"accessing." % (func.__module__, func.__name__))
raise web.seeother('/domains?msg=PERMISSION_DENIED')
# Check whether is domain admin.
is_admin = sql_lib_general.is_domain_admin(domain=domain,
admin=username)
if is_admin:
return func(*args, **kw)
else:
if settings.LOG_PERMISSION_DENIED:
logger.error("PERMISSION_DENIED (2) raised in "
"@require_domain_access, triggered in module: %s.py, "
"function: %s(), accessing data: admin=%s, "
"domain=%s" % (func.__module__, func.__name__, username, domain))
raise web.seeother('/domains?msg=PERMISSION_DENIED')
return proxyfunc
def require_user_login(func):
def proxyfunc(self, *args, **kw):
if session.get('account_is_mail_user'):
return func(self, *args, **kw)
"""
elif session.get('is_normal_admin') and session.get('admin_is_mail_user'):
# Admin manages other domains but not self domain.
# <admin>@<domain.com> doesn't manage <domain.com>
admin = session.get('username')
domain = admin.split('@', 1)[-1]
if not sql_lib_general.is_domain_admin(domain=domain, admin=admin):
return func(self, *args, **kw)
"""
session.kill()
raise web.seeother('/login?msg=LOGIN_REQUIRED')
return proxyfunc
# self-service.
def require_ml_owner_or_moderator(func):
def proxyfunc(*args, **kw):
username = session.get('username')
if not username:
raise web.seeother('/login?msg=LOGIN_REQUIRED')
mail = None
if 'mail' in kw:
# the mailing list
mail = kw['mail']
if not iredutils.is_email(mail):
raise web.seeother("/self-service/mls?msg=INVALID_MAILLIST")
else:
for i in args:
if iredutils.is_email(i):
mail = i
break
if not mail:
raise web.seeother("/self-service/mls?msg=INVALID_MAILLIST")
# Check whether user is an owner or moderator.
_is_owner_or_moderator = sql_lib_general.is_ml_owner_or_moderator(ml=mail, user=username, conn=None)
if _is_owner_or_moderator:
return func(*args, **kw)
else:
if settings.LOG_PERMISSION_DENIED:
logger.error("PERMISSION_DENIED (2) raised in "
"@require_ml_owner_or_moderator, triggered in module: %s.py, "
"function: %s(), accessing data: user=%s, "
"maillist=%s" % (func.__module__, func.__name__, username, mail))
raise web.seeother('/self-service/mls?msg=PERMISSION_DENIED')
return proxyfunc
def api_require_domain_access(func):
def proxyfunc(*args, **kw):
if not iredutils.is_allowed_api_client(web.ctx.ip):
return api_render((False, 'NOT_AUTHORIZED'))
if not session.get('username'):
return api_render((False, 'LOGIN_REQUIRED'))
# Check domain global admin.
if session.get('is_global_admin'):
return func(*args, **kw)
else:
username = session.get('username')
# admin/user is viewing its own data
if username == kw.get('mail') \
or username.endswith('@' + kw.get('domain', 'NONE')):
return func(*args, **kw)
if 'domain' in kw and iredutils.is_domain(kw.get('domain')):
domain = web.safestr(kw['domain'])
elif 'mail' in kw and iredutils.is_email(kw.get('mail')):
domain = web.safestr(kw['mail']).split('@')[-1]
elif 'admin' in kw and iredutils.is_email(kw.get('admin')):
domain = web.safestr(kw['admin']).split('@')[-1]
else:
domain = None
# Try to use the first valid domain name or email address as
# key, it's passed from controllers/*.
for arg in args:
if iredutils.is_domain(arg):
domain = arg
break
elif iredutils.is_email(arg):
domain = arg.split('@', 1)[-1]
break
if not domain:
if settings.LOG_PERMISSION_DENIED:
logger.error("PERMISSION_DENIED (1) raised in "
"@require_domain_access: module=%s.py, "
"function=%s(), admin=%s. "
"No target domain for accessing." % (func.__module__, func.__name__, username))
return api_render((False, 'PERMISSION_DENIED'))
# Check whether is domain admin.
is_admin = sql_lib_general.is_domain_admin(domain=domain,
admin=username)
if is_admin:
return func(*args, **kw)
else:
if settings.LOG_PERMISSION_DENIED:
logger.error("PERMISSION_DENIED (2) raised in "
"@require_domain_access: module=%s.py, "
"function=%s(), "
"admin=%s, "
"domain=%s" % (func.__module__, func.__name__, username, domain))
return api_render((False, 'PERMISSION_DENIED'))
return proxyfunc

2705
libs/sqllib/domain.py Normal file

File diff suppressed because it is too large Load Diff

1084
libs/sqllib/general.py Normal file

File diff suppressed because it is too large Load Diff

1108
libs/sqllib/ml.py Normal file

File diff suppressed because it is too large Load Diff

90
libs/sqllib/sqlutils.py Normal file
View File

@@ -0,0 +1,90 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
from typing import Dict
def account_settings_dict_to_string(account_settings: Dict) -> str:
# Convert account setting dict to string.
# - dict: {'var': 'value', 'var2: value2', ...}
# - string: 'var:value;var2:value2;...'
if not account_settings or not isinstance(account_settings, dict):
return ''
for (k, v) in list(account_settings.items()):
if k in ['default_groups',
'default_mailing_lists',
'enabled_services',
'disabled_mail_services',
'disabled_domain_profiles',
'disabled_user_profiles',
'disabled_user_preferences']:
if isinstance(v, (list, tuple, set)):
if isinstance(v, list):
v.sort()
elif isinstance(v, set):
v = list(v)
v.sort()
account_settings[k] = ','.join(v)
else:
# Remove item if value is not a list/tuple/set
account_settings.pop(k)
new_settings = ';'.join(['{}:{}'.format(str(i), j) for (i, j) in list(account_settings.items()) if j])
if new_settings:
new_settings += ';'
return new_settings
def account_settings_string_to_dict(account_settings: str) -> Dict:
# Convert account setting (string, format 'var:value;var2:value2;...', used
# in MySQL/PGSQL backends) to dict.
# - domain.settings
# - mailbox.settings
# Original setting must be a string
if not account_settings:
return {}
new_settings = {}
items = [st for st in account_settings.split(';') if ':' in st]
for item in items:
if item:
(k, v) = item.split(':')
if v:
new_settings[k] = v
# Convert value to proper format (int, string, ...), default is string.
# It will be useful to compare values with converted values.
# If original value is not stored in proper format, key:value pair will
# be removed.
for key in new_settings:
# integer
if key in ['default_user_quota',
'max_user_quota',
'min_passwd_length',
'max_passwd_length',
# settings used to create new domains.
'create_max_domains',
'create_max_users',
'create_max_lists',
'create_max_aliases',
'create_max_quota']:
try:
new_settings[key] = int(new_settings[key])
except:
new_settings.pop(key)
# list
if key in ['enabled_services',
'disabled_mail_services',
'default_groups',
'default_mailing_lists',
'disabled_domain_profiles',
'disabled_user_profiles',
'disabled_user_preferences']:
new_settings[key] = [str(i) for i in new_settings[key].split(',') if i]
return new_settings

2626
libs/sqllib/user.py Normal file

File diff suppressed because it is too large Load Diff

344
libs/sqllib/utils.py Normal file
View File

@@ -0,0 +1,344 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from libs import iredutils
from libs.logger import log_activity
from libs.sqllib import SQLWrap
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import user as sql_lib_user
from libs.sqllib import alias as sql_lib_alias
from libs.sqllib import ml as sql_lib_ml
from libs.sqllib import general as sql_lib_general
session = web.config.get('_session', {})
def set_account_status(conn,
accounts,
account_type,
enable_account=False):
"""Set account status.
accounts -- an iterable object (list/tuple) filled with accounts.
account_type -- possible value: domain, admin, user, alias, ml
enable_account -- possible value: True, False
"""
if account_type in ['admin', 'user', 'alias', 'maillist', 'ml']:
# email
accounts = [str(v).lower() for v in accounts if iredutils.is_email(v)]
else:
# domain name
accounts = [str(v).lower() for v in accounts if iredutils.is_domain(v)]
if not accounts:
return True,
# 0: disable, 1: enable
account_status = 0
action = 'disable'
if enable_account:
account_status = 1
action = 'active'
if account_type == 'domain':
# handle with function which handles admin privilege
qr = sql_lib_domain.enable_disable_domains(domains=accounts,
action=action)
return qr
elif account_type == 'admin':
# [(<table>, <column-used-for-query>), ...]
table_column_maps = [("admin", "username")]
elif account_type == 'alias':
table_column_maps = [
("alias", "address"),
("forwardings", "address"),
]
elif account_type in ['maillist', 'ml']:
table_column_maps = [("maillists", "address")]
else:
# account_type == 'user'
table_column_maps = [
("mailbox", "username"),
("forwardings", "address"),
]
for (_table, _column) in table_column_maps:
sql_where = '{} IN {}'.format(_column, web.sqlquote(accounts))
try:
conn.update(_table,
where=sql_where,
active=account_status)
except Exception as e:
return False, repr(e)
log_activity(event=action,
msg="{} {}: {}.".format(action.title(), account_type, ', '.join(accounts)))
return True,
def delete_accounts(accounts,
account_type,
keep_mailbox_days=0,
conn=None):
# accounts must be a list/tuple.
# account_type in ['domain', 'user', 'admin', 'alias', 'ml']
if not accounts:
return True,
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
if account_type == 'domain':
qr = sql_lib_domain.delete_domains(domains=accounts,
keep_mailbox_days=keep_mailbox_days,
conn=conn)
return qr
elif account_type == 'user':
sql_lib_user.delete_users(accounts=accounts,
keep_mailbox_days=keep_mailbox_days,
conn=conn)
elif account_type == 'admin':
sql_lib_admin.delete_admins(mails=accounts, conn=conn)
elif account_type == 'alias':
sql_lib_alias.delete_aliases(conn=conn, accounts=accounts)
elif account_type == 'ml':
sql_lib_ml.delete_maillists(conn=conn, accounts=accounts)
return True,
# Search accounts with display name, email.
def search(search_string,
account_type=None,
account_status=None,
conn=None):
"""Return search result in dict.
(True, {
'domain': sql_query_result,
'user': sql_query_result,
...
}
)
"""
sql_vars = {
'search_str': '%%' + search_string + '%%',
'search_str_exclude_domain': '%%' + search_string + '%%@%%',
}
if not account_type:
account_type = ['domain', 'user', 'alias', 'ml', 'admin']
if not account_status:
account_status = ['active', 'disabled']
sql_where_domain_status = ''
sql_where_admin_status = ''
sql_where_user_status = ''
sql_where_ml_status = ''
sql_where_alias_status = ''
sql_where_user_domain = ''
sql_where_alias_domain = ''
sql_where_ml_domain = ''
if 'active' in account_status and 'disabled' in account_status:
pass
elif 'active' in account_status:
sql_where_domain_status = ' AND domain.active=1'
sql_where_admin_status = ' AND domain.active=1'
sql_where_user_status = ' AND mailbox.active=1'
sql_where_alias_status = ' AND alias.active=1'
sql_where_ml_status = ' AND maillists.active=1'
elif 'disabled' in account_status:
sql_where_domain_status = ' AND domain.active=0'
sql_where_admin_status = ' AND domain.active=0'
sql_where_user_status = ' AND mailbox.active=0'
sql_where_alias_status = ' AND alias.active=0'
sql_where_ml_status = ' AND maillists.active=0'
if not conn:
_wrap = SQLWrap()
conn = _wrap.conn
# Get managed domains.
if not session.get('is_global_admin'):
qr = sql_lib_admin.get_managed_domains(admin=session.get('username'),
domain_name_only=True,
listed_only=True,
conn=conn)
if qr[0]:
managed_domains = qr[1]
sql_where_user_domain = ' AND mailbox.domain IN %s' % web.sqlquote(managed_domains)
sql_where_alias_domain = ' AND alias.domain IN %s' % web.sqlquote(managed_domains)
sql_where_ml_domain = ' AND maillists.domain IN %s' % web.sqlquote(managed_domains)
else:
raise web.seeother('/search?msg=%s' % web.urlquote(qr[1]))
result = {
'domain': [],
'admin': [],
'user': [],
'ml': [],
'last_logins': {},
'user_alias_addresses': {},
'user_forwarding_addresses': {},
'user_assigned_groups': {},
'alias': [],
# List of email addresses of global admins.
'allGlobalAdmins': [],
}
if session.get('is_global_admin'):
if 'domain' in account_type:
qr_domain = conn.select(
'domain',
vars=sql_vars,
what='domain,description,aliases,mailboxes,maxquota,active',
where='(domain LIKE $search_str OR description LIKE $search_str) %s' % sql_where_domain_status,
order='domain',
)
if qr_domain:
result['domain'] = iredutils.bytes2str(qr_domain)
if 'admin' in account_type:
qr_admin = conn.select(
'admin',
vars=sql_vars,
what='username,name,active',
where='(username LIKE $search_str OR name LIKE $search_str) %s' % sql_where_admin_status,
order='username',
)
if qr_admin:
result['admin'] = iredutils.bytes2str(qr_admin) or []
# Get all global admin accounts.
qr = sql_lib_admin.get_all_global_admins(conn=conn)
if qr[0]:
result['allGlobalAdmins'] = qr[1]
# Search user accounts.
if 'user' in account_type:
search_str_user = sql_vars['search_str_exclude_domain']
if '@' in sql_vars['search_str']:
search_str_user = sql_vars['search_str']
sql_vars['search_str_user'] = search_str_user
# Query users by email address or display name
qr_user = conn.select(
'mailbox',
vars=sql_vars,
what='username,name,quota,employeeid,active',
where='(username LIKE $search_str_user OR name LIKE $search_str) {} {}'.format(sql_where_user_status, sql_where_user_domain),
order='username')
# Query users by per-user alias address
qr_user_alias = conn.select(
['forwardings', 'mailbox'],
vars=sql_vars,
what='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
where='(forwardings.address LIKE $search_str_user) AND forwardings.forwarding=mailbox.username AND forwardings.is_alias=1 {} {}'.format(sql_where_user_status, sql_where_user_domain),
group='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
order='mailbox.username')
# Query users by mail forwarding address
qr_user_forwarding = conn.select(
['forwardings', 'mailbox'],
vars=sql_vars,
what='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
where='(forwardings.forwarding LIKE $search_str_user) AND forwardings.address=mailbox.username AND forwardings.is_forwarding=1 {} {}'.format(sql_where_user_status, sql_where_user_domain),
group='mailbox.username, mailbox.name, mailbox.quota, mailbox.employeeid, mailbox.active',
order='mailbox.username')
if qr_user:
result['user'] += iredutils.bytes2str(qr_user)
if qr_user_alias:
_records = iredutils.bytes2str(qr_user_alias)
# Add new, remove duplicate records.
for i in _records:
if not (i in result['user']):
result['user'] += [i]
if qr_user_forwarding:
_records = iredutils.bytes2str(qr_user_forwarding)
# Add new, remove duplicate records.
for i in _records:
if not (i in result['user']):
result['user'] += [i]
# Get email addresses of returned user accounts
_user_emails = []
for i in result['user']:
_user_emails.append(str(i['username']).lower())
_user_emails.sort()
# Get per-user alias and mail forwarding addresses
if _user_emails:
(_status, _result) = sql_lib_user.get_bulk_user_alias_addresses(mails=_user_emails, conn=conn)
if _status:
result['user_alias_addresses'] = _result
(_status, _result) = sql_lib_user.get_bulk_user_forwardings(mails=_user_emails, conn=conn)
if _status:
result['user_forwarding_addresses'] = _result
(_status, _result) = sql_lib_user.get_bulk_user_assigned_groups(mails=_user_emails, conn=conn)
if _status:
result['user_assigned_groups'] = _result
# Get user last login
result['last_logins'] = sql_lib_general.get_account_last_login(accounts=_user_emails, conn=conn)
# Search alias accounts.
if 'alias' in account_type:
search_str_alias = sql_vars['search_str_exclude_domain']
if '@' in sql_vars['search_str']:
search_str_alias = sql_vars['search_str']
sql_vars['search_str_alias'] = search_str_alias
qr_alias = conn.select(
'alias',
vars=sql_vars,
what='address,name,accesspolicy,domain,active',
where='(address LIKE $search_str_alias OR name LIKE $search_str) {} {}'.format(
sql_where_alias_status, sql_where_alias_domain,
),
order='address',
)
if qr_alias:
result['alias'] = iredutils.bytes2str(qr_alias) or []
# Search mailing list accounts.
if 'ml' in account_type:
search_str_ml = sql_vars['search_str_exclude_domain']
if '@' in sql_vars['search_str']:
search_str_ml = sql_vars['search_str']
sql_vars['search_str_ml'] = search_str_ml
qr_ml = conn.select(
'maillists',
vars=sql_vars,
what='address,name,accesspolicy,domain,active',
where='(address LIKE $search_str_alias OR name LIKE $search_str) {} {}'.format(
sql_where_ml_status, sql_where_ml_domain,
),
order='address',
)
if qr_ml:
result['ml'] = iredutils.bytes2str(qr_ml) or []
if result:
return True, result
else:
return False, []