Add files via upload

This commit is contained in:
Harold Finch
2023-04-10 07:20:22 +02:00
committed by GitHub
parent 65875d8fef
commit 82d4ef7fa9
100 changed files with 20541 additions and 0 deletions

0
controllers/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,200 @@
import web
from controllers.utils import api_render
from libs import iredutils, form_utils
from libs.amavisd import wblist as lib_wblist
import settings
session = web.config.get('_session')
if settings.backend == 'ldap':
from libs.ldaplib.general import is_domain_admin
else:
from libs.sqllib.general import is_domain_admin
def verify_permission(account):
account = str(account).lower()
if account == 'global':
if not session.get('is_global_admin'):
return False, 'PERMISSION_DENIED'
wblist_account = '@.'
else:
if iredutils.is_domain(account):
domain = account
wblist_account = '@' + account
elif iredutils.is_email(account):
domain = account.split('@', 1)[-1]
wblist_account = account
else:
return False, 'INVALID_ACCOUNT'
if not is_domain_admin(domain=domain, admin=session.get('username'), conn=None):
return False, 'PERMISSION_DENIED'
return True, wblist_account
def get_inout_wb(inout, wb):
_is_in_wl = False
_is_in_bl = False
_is_out_wl = False
_is_out_bl = False
if inout == 'inbound':
if wb == 'whitelist':
_is_in_wl = True
else:
_is_in_bl = True
else:
if wb == 'whitelist':
_is_out_wl = True
else:
_is_out_bl = True
return {'is_in_wl': _is_in_wl,
'is_in_bl': _is_in_bl,
'is_out_wl': _is_out_wl,
'is_out_bl': _is_out_bl}
class APIWBList:
def GET(self, inout, wb, account):
"""Get existing wblist.
curl -X GET -i -b cookie.txt https://<server>/api/wblist/inbound/whitelist/global
curl -X GET -i -b cookie.txt https://<server>/api/wblist/inbound/blacklist/global
curl -X GET -i -b cookie.txt https://<server>/api/wblist/outbound/whitelist/global
curl -X GET -i -b cookie.txt https://<server>/api/wblist/outbound/blacklist/global
"""
_qr = verify_permission(account)
if not _qr[0]:
return api_render(_qr)
wblist_account = _qr[1]
inout_wb = get_inout_wb(inout=inout, wb=wb)
qr = lib_wblist.get_wblist(
account=wblist_account,
whitelist=inout_wb['is_in_wl'],
blacklist=inout_wb['is_in_bl'],
outbound_whitelist=inout_wb['is_out_wl'],
outbound_blacklist=inout_wb['is_out_bl'],
)
if not qr[0]:
return api_render(qr)
result = qr[1]
if inout_wb['is_in_wl']:
addresses = result['inbound_whitelists']
elif inout_wb['is_in_bl']:
addresses = result['inbound_blacklists']
elif inout_wb['is_out_wl']:
addresses = result['outbound_whitelists']
else:
# inout_wb['is_out_bl']
addresses = result['outbound_blacklists']
return api_render((True, addresses))
def POST(self, inout, wb, account):
"""Create new wblist.
curl -X POST ... \
-d "addresses=user@domain.com,user2@domain.com" \
https://<server>/api/wblist/inbound/whitelist/global
curl -X POST ... \
-d "addresses=user@domain.com,user2@domain.com" \
https://<server>/api/wblist/inbound/blacklist/global
curl -X POST ... \
-d "addresses=user@domain.com,user2@domain.com" \
https://<server>/api/wblist/outbound/whitelist/global
curl -X POST ... \
-d "addresses=user@domain.com,user2@domain.com" \
https://<server>/api/wblist/outbound/blacklist/global
"""
_qr = verify_permission(account)
if not _qr[0]:
return api_render(_qr)
wblist_account = _qr[1]
inout_wb = get_inout_wb(inout=inout, wb=wb)
form = web.input(_unicode=False)
_addresses = form_utils.get_multi_values_from_api(form=form, input_name='addresses')
_addresses = [i for i in _addresses if iredutils.is_valid_amavisd_address(i)]
d = {}
for (k, v) in list(inout_wb.items()):
_name = k.replace("is_", "")
if v is True:
d[_name] = _addresses
else:
d[_name] = None
qr = lib_wblist.add_wblist(
account=wblist_account,
wl_senders=d["in_wl"],
bl_senders=d["in_bl"],
wl_rcpts=d["out_wl"],
bl_rcpts=d["out_bl"],
flush_before_import=False,
)
return api_render(qr)
def PUT(self, inout, wb, account):
# Delete addresses
_qr = verify_permission(account)
if not _qr[0]:
return api_render(_qr)
wblist_account = _qr[1]
inout_wb = get_inout_wb(inout=inout, wb=wb)
form = web.input(_unicode=False)
_addresses = form_utils.get_multi_values_from_api(form=form, input_name='addresses')
_addresses = [i for i in _addresses if iredutils.is_valid_amavisd_address(i)]
d = {}
for (k, v) in list(inout_wb.items()):
_name = k.replace("is_", "")
if v is True:
d[_name] = _addresses
else:
d[_name] = None
qr = lib_wblist.delete_wblist(
account=wblist_account,
wl_senders=d["in_wl"],
bl_senders=d["in_bl"],
wl_rcpts=d["out_wl"],
bl_rcpts=d["out_bl"],
)
return api_render(qr)
def DELETE(self, inout, wb, account):
_qr = verify_permission(account)
if not _qr[0]:
return api_render(_qr)
wblist_account = _qr[1]
inout_wb = get_inout_wb(inout=inout, wb=wb)
qr = lib_wblist.delete_all_wblist(
account=wblist_account,
wl_senders=inout_wb['is_in_wl'],
bl_senders=inout_wb['is_in_bl'],
wl_rcpts=inout_wb['is_out_wl'],
bl_rcpts=inout_wb['is_out_bl'],
)
return api_render(qr)

591
controllers/amavisd/log.py Normal file
View File

@@ -0,0 +1,591 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from controllers import decorators
from libs import iredutils
from libs.mailparser import parse_raw_message
from libs.amavisd import QUARANTINE_TYPES
from libs.amavisd import log as lib_log
from libs.amavisd import quarantine as lib_quarantine
from libs.amavisd import wblist as lib_wblist
session = web.config.get('_session')
DELETE_ACTION_MSGS = {
'release': 'RELEASED',
'release_whitelist_sender': 'RELEASED_WL_SENDER',
'release_whitelist_sender_domain': 'RELEASED_WL_SENDER_DOMAIN',
'release_whitelist_sender_subdomain': 'RELEASED_WL_SENDER_SUBDOMAIN',
'delete': 'DELETED',
'deleteAll': 'DELETED',
# log_type == 'received'
'delete_whitelist_sender': 'DELETED_WL_SENDER',
'delete_whitelist_sender_domain': 'DELETED_WL_SENDER_DOMAIN',
'delete_whitelist_sender_subdomain': 'DELETED_WL_SENDER_SUBDOMAIN',
'delete_blacklist_sender': 'DELETED_BL_SENDER',
'delete_blacklist_sender_domain': 'DELETED_BL_SENDER_DOMAIN',
'delete_blacklist_sender_subdomain': 'DELETED_BL_SENDER_SUBDOMAIN',
# log_type == 'sent'
'delete_whitelist_rcpt': 'DELETED_WL_RCPT',
'delete_whitelist_rcpt_domain': 'DELETED_WL_RCPT_DOMAIN',
'delete_whitelist_rcpt_subdomain': 'DELETED_WL_RCPT_SUBDOMAIN',
'delete_blacklist_rcpt': 'DELETED_BL_RCPT',
'delete_blacklist_rcpt_domain': 'DELETED_BL_RCPT_DOMAIN',
'delete_blacklist_rcpt_subdomain': 'DELETED_BL_RCPT_SUBDOMAIN',
}
class InOutMails:
@decorators.require_permission_in_session(perm='disable_viewing_mail_log', not_present=True)
@decorators.require_admin_login
def GET(self, log_type='sent', page=1):
log_type = str(log_type)
# Get current page.
page = int(page) or 1
qr = lib_log.get_in_out_mails(log_type=log_type, cur_page=page)
if qr[0]:
total = qr[1]['count']
records = qr[1]['records']
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
return web.render(
'amavisd/inout.html',
log_type=log_type,
cur_page=page,
account_type=None,
account=None,
total=total,
records=records,
removeLogsInDays=settings.AMAVISD_REMOVE_MAILLOG_IN_DAYS,
msg=web.input().get('msg'),
)
@decorators.csrf_protected
@decorators.require_permission_in_session(perm='disable_viewing_mail_log', not_present=True)
@decorators.require_admin_login
def POST(self, log_type='sent', page=1):
# Get current page.
page = int(page) or 1
redirect_url = '/activities/%s/page/%d' % (log_type, page)
form = web.input(record=[], _unicode=False)
action = form.get('action', 'delete')
if not action.startswith('delete'):
raise web.seeother(redirect_url + '?msg=INVALID_ACTION')
mailids = []
addresses = []
for r in form.get('record', []):
# record format: mail_id + \r\n + sender
tmp = r.split(r'\r\n')
if len(tmp) == 2:
(mid, addr) = tmp
mailids.append(mid)
if iredutils.is_email(addr):
if action.endswith('_sender') or action.endswith('_rcpt'):
addresses.append(addr)
elif action.endswith('_domain'):
addresses.append('@' + addr.split('@', 1)[-1])
elif action.endswith('_subdomain'):
addresses.append('@.' + addr.split('@', 1)[-1])
if (not mailids) and (action != 'deleteAll'):
raise web.seeother(redirect_url + '?msg=INVALID_MAILID')
if action == 'deleteAll':
qr_del = lib_log.delete_all_records(log_type=log_type, account=None)
else:
# delete records by mailids
qr_del = lib_log.delete_records_by_mail_id(log_type=log_type, mail_ids=mailids)
if not qr_del[0]:
raise web.seeother(redirect_url + '?msg=' + web.urlquote(qr_del[1]))
# Add server-wide white/blacklists.
# Note: if admin is a normal admin, we don't know which domain he
# manages, so cannot add per-domain white/blacklists here.
if session.get('is_global_admin') and addresses:
wblist_account = '@.'
# whitelist recipients
if action.startswith('delete_whitelist'):
qr_wblist = lib_wblist.add_wblist(account=wblist_account, wl_senders=addresses)
elif action.startswith('delete_blacklist'):
qr_wblist = lib_wblist.add_wblist(account=wblist_account, bl_senders=addresses)
else:
qr_wblist = (False, 'INVALID_ACTION')
if not qr_wblist[0]:
raise web.seeother(redirect_url + '?msg=' + web.urlquote(qr_wblist[1]))
raise web.seeother(redirect_url + '?msg=' + DELETE_ACTION_MSGS[action])
class InOutMailsPerAccount:
@decorators.require_permission_in_session(perm='disable_viewing_mail_log', not_present=True)
@decorators.require_login
def GET(self, log_type, account_type, account, page=1):
log_type = str(log_type)
account_type = str(account_type)
account = str(account)
page = int(page) or 1
# Verify account syntax
if account_type == 'domain':
if not iredutils.is_domain(account):
raise web.seeother('/activities/%s?msg=INVALID_DOMAIN_NAME' % log_type)
elif account_type == 'user':
if not iredutils.is_email(account):
raise web.seeother('/activities/%s?msg=INVALID_MAIL' % log_type)
qr = lib_log.get_in_out_mails(log_type=log_type,
cur_page=page,
account_type=account_type,
account=account)
if qr[0]:
total = qr[1]['count']
records = qr[1]['records']
else:
raise web.seeother('/activities/{}?msg={}'.format(log_type, web.urlquote(qr[1])))
return web.render(
'amavisd/inout.html',
log_type=log_type,
cur_page=page,
account_type=account_type,
account=account,
total=total,
records=records,
removeLogsInDays=settings.AMAVISD_REMOVE_MAILLOG_IN_DAYS,
msg=web.input().get('msg'),
)
@decorators.csrf_protected
@decorators.require_permission_in_session(perm='disable_viewing_mail_log', not_present=True)
@decorators.require_login
def POST(self, log_type, account_type, account, page=1):
log_type = str(log_type).lower()
account_type = str(account_type).lower()
account = str(account).lower()
page = int(page) or 1
redirect_url = '/activities/{}/{}/{}/page/{}'.format(log_type, account_type, account, page)
form = web.input(record=[], _unicode=False)
action = str(form.get('action', ''))
if not action.startswith('delete'):
raise web.seeother(redirect_url + '?msg=INVALID_ACTION')
mailids = []
addresses = []
for r in form.get('record', []):
# record format: mail_id + \r\n + sender
tmp = r.split(r'\r\n')
if len(tmp) == 2:
(mid, addr) = tmp
mailids.append(mid)
if iredutils.is_email(addr):
if action.endswith('_sender') or action.endswith('_rcpt'):
addresses.append(addr)
elif action.endswith('_domain'):
addresses.append('@' + addr.split('@', 1)[-1])
elif action.endswith('_subdomain'):
addresses.append('@.' + addr.split('@', 1)[-1])
if (not mailids) and (action != 'deleteAll'):
raise web.seeother(redirect_url + '?msg=INVALID_MAILID')
if action == 'deleteAll':
qr_del = lib_log.delete_all_records(log_type=log_type, account=account)
else:
# delete records by mailids
qr_del = lib_log.delete_records_by_mail_id(log_type=log_type, mail_ids=mailids)
if not qr_del[0]:
raise web.seeother(redirect_url + '?msg=' + web.urlquote(qr_del[1]))
# Add server-wide white/blacklists.
# Note: if admin is a normal admin, we don't know which domain he
# manages, so cannot add per-domain white/blacklists here.
if addresses and \
(action.startswith('delete_whitelist') or action.startswith('delete_blacklist')):
wblist_account = None
_do_wb = False
if session.get('is_global_admin'):
# Global wblist
wblist_account = account
_do_wb = True
elif session.get('account_is_mail_user'):
# per-account wblist
wblist_account = session['username']
_do_wb = True
if _do_wb:
# whitelist recipients
if action.startswith('delete_whitelist'):
qr_wblist = lib_wblist.add_wblist(account=wblist_account, wl_senders=addresses)
elif action.startswith('delete_blacklist'):
qr_wblist = lib_wblist.add_wblist(account=wblist_account, bl_senders=addresses)
else:
qr_wblist = (False, 'INVALID_ACTION')
if not qr_wblist[0]:
raise web.seeother(redirect_url + '?msg=' + web.urlquote(qr_wblist[1]))
raise web.seeother(redirect_url + '?msg=' + DELETE_ACTION_MSGS[action])
class QuarantinedMails:
@decorators.require_permission_in_session(perm='disable_managing_quarantined_mails', not_present=True)
@decorators.require_admin_login
def GET(self, quarantined_type=None, page=1):
form = web.input()
sort_by_score = 'sort_by_score' in form
# Get current page.
# None means on page 1, e.g. /activities/quarantined
if quarantined_type in QUARANTINE_TYPES or quarantined_type is None:
page = int(page) or 1
else:
page = int(quarantined_type) or 1
quarantined_type = None
qr = lib_quarantine.get_quarantined_mails(quarantined_type=quarantined_type,
page=page,
sort_by_score=sort_by_score)
if qr[0]:
(total, records) = qr[1]
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
return web.render(
'amavisd/quarantined.html',
account_type=None,
account=None,
quarantined_type=quarantined_type,
cur_page=page,
total=total,
records=records,
removeQuarantinedInDays=settings.AMAVISD_REMOVE_QUARANTINED_IN_DAYS,
sort_by_score=sort_by_score,
msg=form.get('msg'),
)
@decorators.csrf_protected
@decorators.require_permission_in_session(perm='disable_managing_quarantined_mails', not_present=True)
@decorators.require_admin_login
def POST(self, quarantined_type=None, page=1):
form = web.input(record=[], _unicode=False)
action = form.get('action', None)
if quarantined_type not in QUARANTINE_TYPES:
quarantined_type = None
redirect_url = '/activities/quarantined'
if quarantined_type:
redirect_url = redirect_url + '/' + quarantined_type
redirect_url += '/page/{}'.format(page)
if action == 'deleteAll':
if session.get('is_global_admin'):
lib_quarantine.delete_all_quarantined(quarantined_type=quarantined_type)
raise web.seeother(redirect_url + '?msg=%s' % DELETE_ACTION_MSGS[action])
# Get necessary information from web form.
records = []
mailids = []
senders = set()
for r in form.get('record', []):
# record format: mail_id + \r\n + secret_id + \r\n + sender
tmp = r.split(r'\r\n')
if len(tmp) == 3:
records += [{'mail_id': tmp[0], 'secret_id': tmp[1]}]
mailids.append(tmp[0])
if iredutils.is_email(tmp[2]):
senders.add(tmp[2])
if not mailids:
if not (action == 'deleteAll' and session.get('is_global_admin')):
raise web.seeother(redirect_url + '?msg=INVALID_MAILID')
if action != 'deleteAll' and not mailids:
raise web.seeother(redirect_url + '?msg=%s' % DELETE_ACTION_MSGS[action])
wb_senders = set()
if action in ['release_whitelist_sender', 'delete_blacklist_sender']:
wb_senders = senders
elif action in ['release_whitelist_sender_domain', 'delete_blacklist_sender_domain']:
for s in senders:
wb_senders.add('@' + s.split('@', 1)[-1])
elif action in ['release_whitelist_sender_subdomain', 'delete_blacklist_sender_subdomain']:
for s in senders:
wb_senders.add('@.' + s.split('@', 1)[-1])
wblist_account = '@.'
if session.get('is_global_admin'):
# Add as global wblist
wblist_account = '@.'
elif session.get('is_normal_admin'):
# Add as per-domain wblist
wblist_account = '@' + session['username'].split('@', 1)[-1]
if action.startswith('release'):
result = lib_quarantine.release_quarantined_mails(records=records)
if action in ['release_whitelist_sender',
'release_whitelist_sender_domain',
'release_whitelist_sender_subdomain']:
# whitelist senders or sender_domains
if wb_senders:
qr = lib_wblist.add_wblist(account=wblist_account, wl_senders=wb_senders)
if not qr[0]:
result = qr
elif action.startswith('delete'):
result = lib_log.delete_records_by_mail_id(log_type='quarantine', mail_ids=mailids)
if action in ['delete_blacklist_sender',
'delete_blacklist_sender_domain',
'delete_blacklist_sender_subdomain']:
if wb_senders:
qr = lib_wblist.add_wblist(account=wblist_account, bl_senders=wb_senders)
if not qr[0]:
result = qr
else:
result = (False, 'INVALID_ACTION')
if result[0]:
raise web.seeother(redirect_url + '?msg=%s' % DELETE_ACTION_MSGS[action])
else:
raise web.seeother(redirect_url + '?msg=%s' % web.urlquote(result[1]))
class QuarantinedMailsPerAccount:
@decorators.require_permission_in_session(perm='disable_managing_quarantined_mails', not_present=True)
@decorators.require_login
def GET(self, account_type, account, quarantined_type=None, page=1):
account_type = str(account_type)
account = str(account)
form = web.input()
sort_by_score = 'sort_by_score' in form
# Normal user login
if session['account_is_mail_user'] and account_type == 'user':
if session['username'] != account:
# Accessing other's quarantined mails
raise web.seeother('/activities/quarantined/user/%s?msg=PERMISSION_DENIED' % session['username'])
if 'quarantine' in session.get('disabled_user_preferences', []):
raise web.seeother('/preferences?msg=PERMISSION_DENIED')
if quarantined_type:
# Get current page.
if str(quarantined_type).isdigit():
# According to URL mapping, quarantined_type could be page number.
page = int(quarantined_type) or 1
else:
page = int(page) or 1
if quarantined_type not in QUARANTINE_TYPES:
quarantined_type = None
qr = lib_quarantine.get_quarantined_mails(account_type=account_type,
account=account,
quarantined_type=quarantined_type,
page=page,
sort_by_score=sort_by_score)
if qr[0]:
(total, records) = qr[1]
else:
if session['account_is_mail_user']:
raise web.seeother('/preferences?msg=%s' % web.urlquote(qr[1]))
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
template_file = 'amavisd/quarantined.html'
if session['account_is_mail_user']:
template_file = 'amavisd/quarantined_user.html'
return web.render(
template_file,
account_type=account_type,
account=account,
quarantined_type=quarantined_type,
cur_page=page,
total=total,
records=records,
removeQuarantinedInDays=settings.AMAVISD_REMOVE_QUARANTINED_IN_DAYS,
sort_by_score=sort_by_score,
msg=form.get('msg'),
)
@decorators.csrf_protected
@decorators.require_permission_in_session(perm='disable_managing_quarantined_mails', not_present=True)
@decorators.require_login
def POST(self, account_type, account, quarantined_type=None, page=1):
form = web.input(record=[], _unicode=False)
if quarantined_type:
# Get current page.
if str(quarantined_type).isdigit():
# According to URL mapping, quarantined_type could be page number.
page = int(quarantined_type) or 1
else:
page = int(page) or 1
if quarantined_type not in QUARANTINE_TYPES:
quarantined_type = None
redirect_url = '/activities/quarantined'
if account_type and account:
redirect_url = redirect_url + '/{}/{}'.format(account_type, account)
if quarantined_type:
redirect_url = redirect_url + '/' + quarantined_type
redirect_url += '/page/{}'.format(page)
action = form.get('action', None)
# Get necessary information from web form.
records = []
mailids = []
senders = set()
# Get `msgs.mail_id` and `msgs.secret_id`
for r in form.get('record', []):
# record format: mail_id + \r\n + secret_id + \r\n + sender
tmp = r.split(r'\r\n')
if len(tmp) == 3:
records += [{'mail_id': tmp[0], 'secret_id': tmp[1]}]
mailids.append(tmp[0])
if iredutils.is_email(tmp[2]):
senders.add(tmp[2])
if not mailids:
raise web.seeother(redirect_url + '?msg=INVALID_MAILID')
wb_senders = set()
if action in ['release_whitelist_sender', 'delete_blacklist_sender']:
wb_senders = senders
elif action in ['release_whitelist_sender_domain', 'delete_blacklist_sender_domain']:
for s in senders:
wb_senders.add('@' + s.split('@', 1)[-1])
elif action in ['release_whitelist_sender_subdomain', 'delete_blacklist_sender_subdomain']:
for s in senders:
wb_senders.add('@.' + s.split('@', 1)[-1])
wblist_account = account
if session.get('is_global_admin'):
# Add as global wblist
wblist_account = '@.'
elif session.get('is_normal_admin'):
# Add as per-domain wblist
wblist_account = '@' + account.split('@', 1)[-1]
if action.startswith('release'):
result = lib_quarantine.release_quarantined_mails(records=records)
if action in ['release_whitelist_sender',
'release_whitelist_sender_domain',
'release_whitelist_sender_subdomain']:
# whitelist senders or sender_domains
if wb_senders:
qr = lib_wblist.add_wblist(account=wblist_account, wl_senders=wb_senders)
if not qr[0]:
result = qr
elif action.startswith('delete'):
result = lib_log.delete_records_by_mail_id(log_type='quarantine', mail_ids=mailids)
if action in ['delete_blacklist_sender',
'delete_blacklist_sender_domain',
'delete_blacklist_sender_subdomain']:
# Don't add account domain in blacklist
try:
wb_senders.remove(account.split('@', 1)[-1])
except:
pass
if wb_senders:
qr = lib_wblist.add_wblist(account=wblist_account, bl_senders=wb_senders)
if not qr[0]:
result = qr
else:
result = (False, 'INVALID_ACTION')
if result[0]:
msg = DELETE_ACTION_MSGS[action]
else:
msg = web.urlquote(result[1])
raise web.seeother(redirect_url + '?msg=%s' % msg)
class GetRawMessageOfQuarantinedMail:
@decorators.require_login
def GET(self, mail_id):
qr = lib_quarantine.get_raw_message(mail_id=mail_id)
if not qr[0]:
raise web.seeother('/activities/quarantined?msg=%s' % web.urlquote(qr[1]))
# Parse mail and convert to HTML.
try:
(headers, bodies, attachments) = parse_raw_message(qr[1])
except Exception as e:
raise web.seeother('/activities/quarantined?msg=%s' % web.urlquote(repr(e)))
return web.render('amavisd/quarantined_raw.html',
mail_id=mail_id,
headers=headers,
bodies=bodies,
attachments=attachments)
class SearchLog:
@decorators.require_admin_login
def GET(self):
raise web.seeother('/activities/sent')
@decorators.csrf_protected
@decorators.require_admin_login
def POST(self):
form = web.input(_unicode=False)
account = form.get('account', '')
log_type = 'sent'
if 'received' in form:
log_type = 'received'
elif 'sent' in form:
log_type = 'sent'
elif 'quarantined' in form:
log_type = 'quarantined'
if iredutils.is_email(account):
account_type = 'user'
elif iredutils.is_domain(account):
account_type = 'domain'
else:
raise web.seeother('/activities/%s?msg=INVALID_ACCOUNT' % log_type)
raise web.seeother('/activities/{}/{}/{}'.format(log_type, account_type, account))

View File

@@ -0,0 +1,182 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs import iredutils
from controllers import decorators
from libs.amavisd import spampolicy as spampolicylib
# API
from controllers.utils import api_render
if settings.backend == 'ldap':
from libs.ldaplib.general import is_domain_admin
else:
from libs.sqllib.general import is_domain_admin
session = web.config.get('_session')
def _check_privilege(admin, account_type, account):
"""Check whether current admin has privilege to update account spam policy.
Return (True, {'account': xx, 'account_type': xx}) if has required privilege.
Return (False, <reason>) if no required privilege.
"""
if account_type == 'global':
account = '@.'
# Check privilege
if not session.get('is_global_admin'):
return False, 'PERMISSION_DENIED'
elif account_type == 'domain':
domain = account
account = '@' + domain
elif account_type == 'user':
domain = account.split('@', 1)[-1]
else:
return False, 'INVALID_ACCOUNT'
if account_type in ['domain', 'user']:
# Check whether it's managed by admin
if not is_domain_admin(domain=domain, admin=admin):
return False, 'PERMISSION_DENIED'
return True, {'account': account, 'account_type': account_type}
class SpamPolicy:
def _get_account_and_type(self):
# account, type:
# - @.: global
# - domain.com: domain
# - user@domain.com: user, user_preference
current_url = web.ctx.environ['PATH_INFO']
if current_url == '/system/spampolicy':
# Global policy
account = '@.'
account_type = 'global'
elif current_url.startswith('/profile/domain'):
# Per-domain policy
account = '@' + current_url.split('/')[-1]
account_type = 'domain'
elif current_url.startswith('/profile/user'):
# per-user policy, modifying by admin.
account = current_url.split('/')[-1]
account_type = 'user'
else:
# per-user preferences
# web.ctx.PATH_INFO == '/preferences/spampolicy'
account = session['username']
account_type = 'user_preference'
return {'account': account,
'account_type': account_type,
'url': current_url}
@decorators.require_preference_access('spampolicy')
@decorators.require_login
def GET(self, account=None):
d = self._get_account_and_type()
account = d['account']
account_type = d['account_type']
current_url = d['url']
if account_type == 'global':
# Check privilege
if not session.get('is_global_admin'):
raise web.seeother('/domains?msg=PERMISSION_DENIED')
elif account_type in ['domain', 'user']:
domain = account.split('@', 1)[-1]
# Check whether it's managed by admin
if not is_domain_admin(domain=domain, admin=session.get('username')):
raise web.seeother('/domains?msg=PERMISSION_DENIED')
(success, policy) = spampolicylib.get_spam_policy(account=account)
if not success:
if account_type == 'user_preference':
raise web.seeother('/preferences?msg=%s' % web.urlquote(policy))
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(policy))
global_spam_score = spampolicylib.get_global_spam_score()
return web.render(
'amavisd/spampolicy.html',
account_type=account_type,
spampolicy=policy,
global_spam_score=global_spam_score,
custom_ban_rules=settings.AMAVISD_BAN_RULES,
current_url=current_url,
msg=web.input().get('msg'),
)
@decorators.require_preference_access('spampolicy')
@decorators.require_login
def POST(self, account=None):
if account:
if iredutils.is_domain(account):
policy_account = '@' + account
current_url = '/profile/domain/spampolicy/' + account
elif iredutils.is_email(account):
policy_account = str(account)
current_url = '/profile/user/spampolicy/' + policy_account
else:
d = self._get_account_and_type()
policy_account = d['account']
current_url = d['url']
form = web.input(banned_rulenames=[])
qr = spampolicylib.update_spam_policy(account=policy_account, form=form)
if qr[0]:
raise web.seeother(current_url + '?msg=UPDATED')
else:
raise web.seeother(current_url + '?msg=%s' % web.urlquote(qr[1]))
class APISpamPolicy:
@decorators.require_preference_access('spampolicy')
@decorators.require_login
def GET(self, account_type, account=None):
qr = _check_privilege(admin=session.get('username'),
account_type=account_type,
account=account)
if not qr[0]:
return api_render(qr)
account = qr[1]['account']
qr = spampolicylib.get_spam_policy(account=account)
return api_render(qr)
@decorators.require_preference_access('spampolicy')
@decorators.require_login
def PUT(self, account_type, account=None):
qr = _check_privilege(admin=session.get('username'),
account_type=account_type,
account=account)
if not qr[0]:
return api_render(qr)
form = web.input(_unicode=False)
account = qr[1]['account']
qr = spampolicylib.api_update_spam_policy(account=account, form=form)
return api_render(qr)
@decorators.require_preference_access('spampolicy')
@decorators.require_login
def DELETE(self, account_type, account=None):
qr = _check_privilege(admin=session.get('username'),
account_type=account_type,
account=account)
if not qr[0]:
return api_render(qr)
account = qr[1]['account']
qr = spampolicylib.delete_spam_policy(account=account)
return api_render(qr)

View File

@@ -0,0 +1,73 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import settings
from libs.regxes import email as e, domain as d
# fmt: off
urls = [
# Search activity logs.
'/activities/search', 'controllers.amavisd.log.SearchLog',
# View log of sent/received mails
'/activities/(received|sent)', 'controllers.amavisd.log.InOutMails',
r'/activities/(received|sent)/page/(\d+)', 'controllers.amavisd.log.InOutMails',
# Per-user activities
'/activities/(received|sent)/(user)/(%s)' % e, 'controllers.amavisd.log.InOutMailsPerAccount',
r'/activities/(received|sent)/(user)/(%s)/page/(\d+)' % e, 'controllers.amavisd.log.InOutMailsPerAccount',
# Per-domain activities
'/activities/(received|sent)/(domain)/(%s)' % d, 'controllers.amavisd.log.InOutMailsPerAccount',
r'/activities/(received|sent)/(domain)/(%s)/page/(\d+)' % d, 'controllers.amavisd.log.InOutMailsPerAccount',
# Quarantined mails
'/activities/quarantined', 'controllers.amavisd.log.QuarantinedMails',
r'/activities/quarantined/page/(\d+)', 'controllers.amavisd.log.QuarantinedMails',
'/activities/quarantined/(spam|virus|banned|badheader|badmime|clean)', 'controllers.amavisd.log.QuarantinedMails',
r'/activities/quarantined/(spam|virus|banned|badheader|badmime|clean)/page/(\d+)', 'controllers.amavisd.log.QuarantinedMails',
# Per-user quarantined mails
r'/activities/quarantined/(user)/(%s)' % e, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
r'/activities/quarantined/(user)/(%s)/page/(\d+)' % e, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
'/activities/quarantined/(user)/(%s)/(spam|virus|banned|badheader|badmime|clean)' % e, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
r'/activities/quarantined/(user)/(%s)/(spam|virus|banned|badheader|badmime|clean)/page/(\d+)' % e, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
# Per-domain quarantined mails
'/activities/quarantined/(domain)/(%s)' % d, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
r'/activities/quarantined/(domain)/(%s)/page/(\d+)' % d, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
'/activities/quarantined/(domain)/(%s)/(spam|virus|banned|badheader|badmime|clean)' % d, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
r'/activities/quarantined/(domain)/(%s)/(spam|virus|banned|badheader|badmime|clean)/page/(\d+)' % d, 'controllers.amavisd.log.QuarantinedMailsPerAccount',
# Get RAW message of quarantined mail by mail_id.
'/activities/quarantined/raw/(.*)', 'controllers.amavisd.log.GetRawMessageOfQuarantinedMail',
# Activity management
'/activities/sender/(%s)' % e, 'controllers.amavisd.log.ActivityManagement',
# Spam policies.
# Global spam policy (recipient = '@.')
'/system/spampolicy', 'controllers.amavisd.spampolicy.SpamPolicy',
# per-domain spam policy (recipient = '@domain.com')
'/system/spampolicy/(%s$)' % d, 'controllers.amavisd.spampolicy.SpamPolicy',
# per-user spam policy (recipient = '@domain.com')
'/system/spampolicy/(%s$)' % e, 'controllers.amavisd.spampolicy.SpamPolicy',
# global wblist
'/create/wblist', 'controllers.amavisd.wblist.Create',
'/system/wblist', 'controllers.amavisd.wblist.GlobalWBList',
# Per-user preferences: wblist, spam control
'/preferences/wblist', 'controllers.amavisd.wblist.UserWBList',
'/preferences/spampolicy', 'controllers.amavisd.spampolicy.SpamPolicy',
]
# API Interfaces
if settings.ENABLE_RESTFUL_API:
urls += [
# Global, per-domain, per-user spam policy
'/api/spampolicy/(global)', 'controllers.amavisd.spampolicy.APISpamPolicy',
'/api/spampolicy/(domain)/(%s$)' % d, 'controllers.amavisd.spampolicy.APISpamPolicy',
'/api/spampolicy/(user)/(%s$)' % e, 'controllers.amavisd.spampolicy.APISpamPolicy',
'/api/wblist/(inbound|outbound)/(whitelist|blacklist)/(global)', 'controllers.amavisd.api_wblist.APIWBList',
'/api/wblist/(inbound|outbound)/(whitelist|blacklist)/(%s$)' % d, 'controllers.amavisd.api_wblist.APIWBList',
'/api/wblist/(inbound|outbound)/(whitelist|blacklist)/(%s$)' % e, 'controllers.amavisd.api_wblist.APIWBList',
]
# fmt: on

View File

@@ -0,0 +1,104 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from controllers import decorators
from libs.amavisd import get_wblist_from_form, wblist as lib_wblist
session = web.config.get('_session')
def render_wblist(account, template):
whitelists = []
blacklists = []
outbound_whitelists = []
outbound_blacklists = []
qr = lib_wblist.get_wblist(account=account)
if qr[0]:
whitelists = qr[1]['inbound_whitelists']
blacklists = qr[1]['inbound_blacklists']
outbound_whitelists = qr[1]['outbound_whitelists']
outbound_blacklists = qr[1]['outbound_blacklists']
return web.render(template,
whitelists=whitelists,
blacklists=blacklists,
outbound_whitelists=outbound_whitelists,
outbound_blacklists=outbound_blacklists,
msg=web.input().get('msg'))
def update_wblist_from_form(form,
account,
post_url,
success_msg,
flush_before_import=False):
wl_senders = get_wblist_from_form(form, 'wl_sender')
bl_senders = get_wblist_from_form(form, 'bl_sender')
wl_rcpts = get_wblist_from_form(form, 'wl_rcpt')
bl_rcpts = get_wblist_from_form(form, 'bl_rcpt')
qr = lib_wblist.add_wblist(account=account,
wl_senders=wl_senders,
bl_senders=bl_senders,
wl_rcpts=wl_rcpts,
bl_rcpts=bl_rcpts,
flush_before_import=flush_before_import)
if qr[0]:
raise web.seeother(post_url + '?msg=' + success_msg)
else:
raise web.seeother(post_url + '?msg=%s' % web.urlquote(qr[1]))
# Add global white/blacklists
class Create:
@decorators.require_global_admin
def GET(self):
return web.render('amavisd/wblist/create.html',
msg=web.input().get('msg'))
@decorators.require_global_admin
def POST(self):
form = web.input()
return update_wblist_from_form(form=form,
account='@.',
post_url='/create/wblist',
success_msg='WBLIST_CREATED',
flush_before_import=False)
class GlobalWBList:
@decorators.require_global_admin
def GET(self):
return render_wblist(account='@.', template='amavisd/wblist/global.html')
@decorators.require_global_admin
def POST(self):
form = web.input()
return update_wblist_from_form(form=form,
account='@.',
post_url='/system/wblist',
success_msg='WBLIST_UPDATED',
flush_before_import=True)
class UserWBList:
@decorators.require_preference_access('wblist')
@decorators.require_login
def GET(self):
account = session['username']
return render_wblist(account=account,
template='amavisd/wblist/user.html')
@decorators.require_preference_access('wblist')
@decorators.require_login
def POST(self):
account = session['username']
form = web.input()
return update_wblist_from_form(form=form,
account=account,
post_url='/preferences/wblist',
success_msg='WBLIST_UPDATED',
flush_before_import=True)

175
controllers/decorators.py Normal file
View File

@@ -0,0 +1,175 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from libs import iredutils
from libs.logger import logger
from controllers.utils import api_render
import settings
session = web.config.get("_session")
def require_login(func):
def proxyfunc(*args, **kw):
if session.get("logged"):
return func(*args, **kw)
else:
session.kill()
raise web.seeother("/login?msg=LOGIN_REQUIRED")
return proxyfunc
def require_admin_login(func):
def proxyfunc(*args, **kw):
if session.get("logged"):
if session.get("is_global_admin") or session.get("is_normal_admin"):
return func(*args, **kw)
else:
if session.get("account_is_mail_user"):
raise web.seeother("/preferences?msg=PERMISSION_DENIED")
else:
raise web.seeother("/domains?msg=PERMISSION_DENIED")
else:
session.kill()
raise web.seeother("/login?msg=LOGIN_REQUIRED")
return proxyfunc
def api_require_admin_login(func):
def proxyfunc(*args, **kw):
if session.get("logged"):
if session.get("is_global_admin") or session.get("is_normal_admin"):
return func(*args, **kw)
else:
session.kill()
return api_render((False, "LOGIN_REQUIRED"))
else:
session.kill()
return api_render((False, "LOGIN_REQUIRED"))
return proxyfunc
def require_global_admin(func):
def proxyfunc(*args, **kw):
if session.get("is_global_admin"):
return func(*args, **kw)
else:
if session.get("logged"):
if session.get("account_is_mail_user"):
raise web.seeother("/preferences?msg=PERMISSION_DENIED")
else:
raise web.seeother("/domains?msg=PERMISSION_DENIED")
else:
raise web.seeother("/login?msg=LOGIN_REQUIRED")
return proxyfunc
def api_require_global_admin(func):
if not iredutils.is_allowed_api_client(web.ctx.ip):
return api_render((False, "NOT_AUTHORIZED"))
def proxyfunc(*args, **kw):
if session.get("is_global_admin"):
return func(*args, **kw)
else:
if session.get("username"):
return api_render((False, "PERMISSION_DENIED"))
else:
return api_render((False, "LOGIN_REQUIRED"))
return proxyfunc
def require_user_login(func):
def proxyfunc(*args, **kw):
if session.get("account_is_mail_user"):
return func(*args, **kw)
else:
session.kill()
raise web.seeother("/login?msg=LOGIN_REQUIRED")
return proxyfunc
def csrf_protected(f):
def decorated(*args, **kw):
form = web.input()
if "csrf_token" not in form:
return web.render("error_csrf.html")
if not session.get("csrf_token"):
session["csrf_token"] = iredutils.generate_random_strings(32)
if form["csrf_token"] != session["csrf_token"]:
return web.render("error_csrf.html")
return f(*args, **kw)
return decorated
# Used in user self-service
def require_preference_access(preference):
def proxyfunc1(func):
def proxyfunc2(*args, **kw):
return func(*args, **kw)
return proxyfunc2
if session.get("is_global_admin") or session.get("is_normal_admin"):
return proxyfunc1
else:
# session.get('account_is_mail_user')
if preference in session.get("disabled_user_preferences", []):
raise web.seeother("/preferences?msg=PERMISSION_DENIED")
else:
return proxyfunc1
def require_permission_create_domain(func):
def proxyfunc(*args, **kw):
if session.get("is_global_admin") or session.get("create_new_domains"):
return func(*args, **kw)
else:
if session.get("account_is_mail_user"):
raise web.seeother("/preferences?msg=PERMISSION_DENIED")
else:
raise web.seeother("/domains?msg=PERMISSION_DENIED")
return proxyfunc
def require_permission_in_session(perm, present=False, not_present=False, value=""):
def proxyfunc(func):
def proxyargs(*args, **kwargs):
if present:
if perm in session:
return func(*args, **kwargs)
if not_present:
if perm not in session:
return func(*args, **kwargs)
if value:
if session.get(perm) == value:
return func(*args, **kwargs)
if settings.LOG_PERMISSION_DENIED:
logger.error("PERMISSION_DENIED raised in decorator "
"@require_permission_in_session: module=%s.py, "
"function=%s(), "
"permission=%s" % (func.__module__, func.__name__, perm))
if session.get("account_is_mail_user"):
raise web.seeother("/preferences?msg=PERMISSION_DENIED")
else:
raise web.seeother("/domains?msg=PERMISSION_DENIED")
return proxyargs
return proxyfunc

View File

View File

@@ -0,0 +1,11 @@
from controllers import decorators
from controllers.utils import api_render
from libs.f2b import log as f2b_log
class APIBannedCount:
@decorators.api_require_global_admin
def GET(self):
total = f2b_log.num_banned()
return api_render((True, total))

81
controllers/f2b/log.py Normal file
View File

@@ -0,0 +1,81 @@
from base64 import b64decode
import web
from controllers import decorators
from controllers.utils import api_render
from libs import iredutils
from libs.logger import log_activity
class Banned:
@decorators.require_global_admin
def GET(self):
_qr = web.conn_f2b.select(
'banned',
what='id, ip, rdns, ports, jail, country, failures, timestamp, remove',
order='ip',
)
rows = list(_qr)
return web.render('fail2ban/banned.html', rows=rows)
class UnbanIP:
"""Unban given IP address, or the IP addresses submitted by form.
Note: It returns JSON.
"""
@decorators.require_global_admin
def DELETE(self, ip=None):
if ip:
ips = [ip]
else:
# Get IP addresses from web form.
form = web.input(ip=[])
ips = form.get('ip', [])
ips = [ip for ip in ips if iredutils.is_strict_ip(ip)]
if not ips:
return api_render(True)
try:
web.conn_f2b.update(
'banned',
vars={"ips": ips},
remove=1,
where="ip IN $ips",
)
log_activity(msg="Unbanned: " + ', '.join(ips),
event='unban')
return api_render(True)
except Exception as e:
return api_render((False, repr(e)))
class MatchedLogLines:
@decorators.require_global_admin
def GET(self, record_id):
_qr = web.conn_f2b.select(
'banned',
vars={'id': record_id},
what='loglines',
where='id=$id',
limit=1,
)
if _qr:
loglines = _qr[0]['loglines']
# Assume its base64 encoded, try to decode it.
if loglines:
try:
loglines = iredutils.bytes2str(b64decode(loglines))
except:
pass
else:
loglines = 'NO_MATCHED_LOG_LINES'
return web.render('fail2ban/matched_log_lines.html', loglines=loglines)

14
controllers/f2b/urls.py Normal file
View File

@@ -0,0 +1,14 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
# fmt: off
urls = [
'/activities/fail2ban/banned', 'controllers.f2b.log.Banned',
r'/activities/fail2ban/banned/loglines/(\d+)', 'controllers.f2b.log.MatchedLogLines',
# Warning: it returns JSON.
'/activities/fail2ban/unbanip/(.*)', 'controllers.f2b.log.UnbanIP',
# API interfaces used by web ui.
'/api/activities/fail2ban/banned/count', 'controllers.f2b.api_log.APIBannedCount',
]
# fmt: on

View File

View File

@@ -0,0 +1,416 @@
import web
from controllers.utils import api_render
from libs import iredutils
from libs.iredapd import greylist as lib_greylist
import settings
if settings.backend == 'ldap':
from libs.ldaplib import decorators
else:
from libs.sqllib import decorators
def convert_greylist_setting_to_api_json(greylist_setting=None):
"""Return dict with simplified information as API result."""
if not greylist_setting:
greylist_setting = {}
_status = greylist_setting.get('active', 'inherit')
status = 'inherit'
if _status == 1:
status = 'enabled'
elif _status == 0:
status = 'disabled'
return api_render((True, {'status': status}))
class APIAllSettings:
@decorators.api_require_global_admin
def GET(self):
"""Get all existing greylisting settings.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/all
"""
s = lib_greylist.get_all_greylist_settings()
_all_settings = {}
for i in s:
_sender = str(i.sender).lower()
_account = str(i.account).lower()
_active = int(i.active)
_setting = {'sender': _sender,
'account': _account}
if _active == 1:
_setting['status'] = 'enabled'
else:
_setting['status'] = 'disabled'
if _account in _all_settings:
_all_settings[_account] += [_setting]
else:
_all_settings[_account] = [_setting]
return api_render((True, _all_settings))
class APIGlobalSetting:
@decorators.api_require_global_admin
def GET(self):
"""Get global greylisting setting.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/global
"""
s = lib_greylist.get_greylist_setting(account='@.')
# If no greylisting setting, mark it as explicitly disabled.
if not s:
s = {'active': 0}
return convert_greylist_setting_to_api_json(s)
@decorators.api_require_global_admin
def POST(self):
"""Set global greylisting setting.
curl -X POST -i -b cookie.txt -d "status=enable" https://<server>/api/greylisting/global
Required parameters:
@status -- Explicitly enable or disable greylisting globally.
Possible values: enable, disable.
"""
form = web.input(_unicode=False)
enable = True
if form.get('status') == 'disable':
enable = False
qr = lib_greylist.enable_disable_greylist_setting(account='@.', enable=enable)
return api_render(qr)
class APIDomainSetting:
@decorators.api_require_domain_access
def GET(self, domain):
"""Get per-domain greylisting setting.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/<domain>
"""
domain = str(domain).lower()
s = lib_greylist.get_greylist_setting(account='@' + domain)
return convert_greylist_setting_to_api_json(s)
@decorators.api_require_domain_access
def POST(self, domain):
"""Set per-domain greylisting setting.
curl -X POST -i -b cookie.txt -d "status=enable" https://<server>/api/greylisting/<domain>
Required parameters:
@status -- Explicitly enable or disable greylisting globally.
Possible values: enable, disable.
"""
form = web.input(_unicode=False)
domain = str(domain).lower()
status = form.get('status', 'inherit').lower()
if status in ['enable', 'disable']:
enable = (status == 'enable')
qr = lib_greylist.enable_disable_greylist_setting(account='@' + domain, enable=enable)
else:
# Remove setting
qr = lib_greylist.delete_greylist_setting(account='@' + domain)
return api_render(qr)
class APIUserSetting:
@decorators.api_require_domain_access
def GET(self, mail):
"""Get per-user greylisting setting.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/<mail>
"""
mail = str(mail).lower()
s = lib_greylist.get_greylist_setting(account=mail)
return convert_greylist_setting_to_api_json(s)
@decorators.api_require_domain_access
def POST(self, mail):
"""Set per-user greylisting setting.
curl -X POST -i -b cookie.txt -d "status=enable" https://<server>/api/greylisting/<mail>
Required parameters:
@status -- Explicitly enable or disable greylisting globally.
Possible values: enable, disable.
"""
form = web.input(_unicode=False)
status = form.get('status', 'inherit').lower()
mail = str(mail).lower()
if status in ['enable', 'disable']:
enable = (status == 'enable')
qr = lib_greylist.enable_disable_greylist_setting(account=mail, enable=enable)
else:
# Remove setting
qr = lib_greylist.delete_greylist_setting(account=mail)
return api_render(qr)
def _get_account_whitelists(account):
account = str(account).lower()
if not (iredutils.is_domain(account)
or iredutils.is_email(account)
or account == '@.'):
return False, 'INVALID_ACCOUNT'
if iredutils.is_domain(account):
account = '@' + account
wl = lib_greylist.get_greylist_whitelists(account=account, address_only=True)
_result = {'whitelists': wl}
if account == '@.':
wl_domains = lib_greylist.get_greylist_whitelist_domains()
_result['whitelist_domains'] = wl_domains
return True, _result
def _update_account_whitelists(account, form):
account = str(account).lower()
if not (iredutils.is_domain(account)
or iredutils.is_email(account)
or account == '@.'):
return False, 'INVALID_ACCOUNT'
if iredutils.is_domain(account):
account = '@' + account
if 'senders' in form:
# Reset whitelisted senders
_senders = form.get('senders', '').strip().split(',')
_senders = [str(i).lower()
for i in _senders
if iredutils.is_valid_wblist_address(i)]
_senders = list(set(_senders))
qr = lib_greylist.reset_greylist_whitelists(account=account,
whitelists=_senders)
if not qr[0]:
return qr
else:
# Add new whitelist senders
_new = []
if 'addSenders' in form:
_new = form.get('addSenders', '').strip().split(',')
# Remove existing ones
_removed = []
if 'removeSenders' in form:
_removed = form.get('removeSenders', '').strip().split(',')
qr = lib_greylist.update_greylist_whitelists(account=account,
new=_new,
removed=_removed)
if not qr[0]:
return qr
return True,
class APIGlobalWhitelists:
@decorators.api_require_global_admin
def GET(self):
"""Get globally whitelisted senders for greylisting service.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/global/whitelists
"""
qr = _get_account_whitelists(account='@.')
return api_render(qr)
@decorators.api_require_global_admin
def POST(self):
"""Set global greylisting setting.
curl -X POST -i -b cookie.txt -d "var=value&var2=value2" https://<server>/api/greylisting/global/whitelists
Optional parameters:
@senders - Reset whitelisted senders for global greylisting
service to given senders. Multiple addresses must
be separated by comma. Conflicts with parameter
`addSenders` and `removeSenders`.
@addSenders - Whitelist new senders for greylisting service
globally. Multiple addresses must be separated by
comma. Conflicts with parameter `senders`.
@removeSenders - Remove existing whitelisted senders for
greylisting service globally. Multiple
addresses must be separated by comma.
Conflicts with parameter `senders`.
"""
form = web.input(_unicode=False)
qr = _update_account_whitelists(account='@.', form=form)
if not qr[0]:
return api_render(qr)
return api_render(True)
class APIGlobalWhitelist:
"""Handle single whitelist."""
@decorators.api_require_global_admin
def PUT(self, ip):
"""
Whitelist given IP address globally.
curl -X PUT -i -b cookie.txt https://<server>/api/greylisting/global/whitelist/<ip>
"""
qr = lib_greylist.update_greylist_whitelists(account='@.', new=[ip], removed=None)
return api_render(qr)
class APIDomainWhitelists:
@decorators.api_require_domain_access
def GET(self, domain):
"""Get whitelisted senders for greylisting service for given domain.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/<domain>/whitelists
"""
qr = _get_account_whitelists(account=domain)
return api_render(qr)
@decorators.api_require_domain_access
def POST(self, domain):
"""Set global greylisting setting.
curl -X POST -i -b cookie.txt -d "var=value&var2=value2" https://<server>/api/greylisting/<domain>/whitelists
Optional parameters:
@senders - Reset whitelisted senders
@addSenders - Whitelist new senders for greylisting service
@removeSenders - Remove existing whitelisted senders
"""
form = web.input(_unicode=False)
qr = _update_account_whitelists(account=domain, form=form)
if not qr[0]:
return api_render(qr)
return api_render(True)
class APIUserWhitelists:
@decorators.api_require_domain_access
def GET(self, mail):
"""Get whitelisted senders for greylisting service for given user.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/<mail>/whitelists
"""
qr = _get_account_whitelists(account=mail)
return api_render(qr)
@decorators.api_require_domain_access
def POST(self, mail):
"""Set global greylisting setting.
curl -X POST -i -b cookie.txt -d "var=value&var2=value2" https://<server>/api/greylisting/<mail>/whitelists
Optional parameters:
@senders - Reset whitelisted senders
@addSenders - Whitelist new senders for greylisting service
@removeSenders - Remove existing whitelisted senders
"""
form = web.input(_unicode=False)
qr = _update_account_whitelists(account=mail, form=form)
if not qr[0]:
return api_render(qr)
return api_render(True)
def _update_whitelist_spf_domains(form):
if 'domains' in form:
# Reset
_domains = form.get('domains', '').strip().split(',')
_domains = [str(i).lower()
for i in _domains
if iredutils.is_domain(i)]
_domains = list(set(_domains))
qr = lib_greylist.reset_greylist_whitelist_domains(domains=_domains)
if not qr[0]:
return qr
else:
# Add new
_new = []
if 'addDomains' in form:
_new = form.get('addDomains', '').strip().split(',')
# Remove existing ones
_removed = []
if 'removeDomains' in form:
_removed = form.get('removeDomains', '').strip().split(',')
qr = lib_greylist.update_greylist_whitelist_domains(new=_new, removed=_removed)
if not qr[0]:
return qr
return True,
class APIWhitelistSPFDomain:
@decorators.api_require_global_admin
def GET(self):
"""Get whitelisted sender domains (for SPF query) for greylisting service.
curl -X GET -i -b cookie.txt https://<server>/api/greylisting/whitelist_spf_domains
"""
domains = lib_greylist.get_greylist_whitelist_domains()
return api_render((True, {'domains': domains}))
@decorators.api_require_global_admin
def POST(self):
"""Manage whitelisted sender domains (for SPF query) for greylisting service.
curl -X POST -i -b cookie.txt -d "var=value&var2=value2" https://<server>/api/greylisting/whitelist_spf_domains
Optional parameters:
@domains - Reset sender domains
@addDomains - Add new sender domains
@removeDomains - Remove existing sender domains
Note: given sender domain names are not used directly while checking
whitelisting, instead, there's a cron job to query SPF and MX
DNS records of given sender domains, then whitelist the IP
addresses/networks listed in DNS records. Multiple domains must
be separated by comma.
"""
form = web.input(_unicode=False)
qr = _update_whitelist_spf_domains(form)
return api_render(qr)

View File

@@ -0,0 +1,118 @@
import web
from controllers.utils import api_render
from libs import form_utils
from libs.iredapd import throttle as iredapd_throttle
import settings
if settings.backend == 'ldap':
from libs.ldaplib import decorators
else:
from libs.sqllib import decorators
# TODO able to specify quota unit for msg_size and max_quota. e.g. 10MB, 2GB.
# Build form from API POST data and submit the throttle setting
def _add_throttle(form, account, kind):
form['enable_' + kind + '_throttling'] = 'on'
if 'period' in form:
form[kind + '_period'] = form.pop('period')
else:
return False, 'MISS_PERIOD'
_has_rule = False
for i in ['msg_size', 'max_quota', 'max_msgs']:
if i in form:
_has_rule = True
# radio/checkboxes are toggled
form[kind + '_' + i] = 'on'
# value
form['custom_' + kind + '_' + i] = form.pop(i)
if not _has_rule:
return False, 'MISS_THROTTLE_SETTING'
ts = form_utils.get_throttle_setting(form, account=account, inout_type=kind)
qr = iredapd_throttle.add_throttle(account=account, setting=ts, inout_type=kind)
return qr
class APIGlobalThrottle:
@decorators.require_global_admin
def GET(self, kind):
"""Get global inbound and outbound throttle settings.
curl -X GET -i -b cookie.txt https://<server>/api/throttle/global/inbound
curl -X GET -i -b cookie.txt https://<server>/api/throttle/global/outbound
"""
ts = iredapd_throttle.get_throttle_setting(account='@.', inout_type=kind)
return api_render({'_success': True, 'setting': ts})
@decorators.require_global_admin
def POST(self, kind):
"""Set global throttle settings.
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/global/inbound
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/global/outbound
Required POST parameters:
@period - Period of time (in seconds)
@msg_size - Max size of single email
@max_msgs - Number of max inbound emails
@max_quota - Cumulative size of all inbound emails
Note: at least one of msg_size, max_msgs, max_quota is required.
"""
form = web.input(_unicode=False)
qr = _add_throttle(form, account='@.', kind=kind)
return api_render(qr)
class APIDomainThrottle:
@decorators.api_require_domain_access
def GET(self, domain, kind):
"""Set per-domain throttle settings.
curl -X GET -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<domain>/inbound
curl -X GET -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<domain>/outbound
"""
ts = iredapd_throttle.get_throttle_setting(account='@' + domain, inout_type=kind)
return api_render({'_success': True, 'setting': ts})
@decorators.api_require_domain_access
def POST(self, domain, kind):
"""Set per-domain throttle settings.
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<domain>/inbound
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<domain>/outbound
"""
form = web.input(_unicode=False)
qr = _add_throttle(form, account='@' + domain, kind=kind)
return api_render(qr)
class APIUserThrottle:
@decorators.api_require_domain_access
def GET(self, mail, kind):
"""Set per-user throttle settings.
curl -X GET -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<mail>/inbound
curl -X GET -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<mail>/outbound
"""
ts = iredapd_throttle.get_throttle_setting(account=mail, inout_type=kind)
return api_render({'_success': True, 'setting': ts})
@decorators.api_require_domain_access
def POST(self, mail, kind):
"""Set per-user throttle settings.
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<mail>/inbound
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/throttle/<mail>/outbound
"""
form = web.input(_unicode=False)
qr = _add_throttle(form, account=mail, kind=kind)
return api_render(qr)

View File

@@ -0,0 +1,55 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from libs.iredapd import greylist as iredapd_greylist
import settings
if settings.backend == 'ldap':
from libs.ldaplib import decorators
else:
from libs.sqllib import decorators
class DefaultGreylisting:
@decorators.require_global_admin
def GET(self):
gl_setting = iredapd_greylist.get_greylist_setting(account='@.')
gl_whitelists = iredapd_greylist.get_greylist_whitelists(account='@.')
gl_whitelist_domains = iredapd_greylist.get_greylist_whitelist_domains()
# Get greylisting tracking data
(_status, _result) = iredapd_greylist.get_tracking_data(account='@.')
if not _status:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
else:
tracking_records = _result
return web.render('iredapd/greylisting_global.html',
gl_setting=gl_setting,
gl_whitelists=gl_whitelists,
gl_whitelist_domains=gl_whitelist_domains,
parent_setting={},
tracking_records=tracking_records,
msg=web.input().get('msg'))
@decorators.require_global_admin
def POST(self):
form = web.input()
qr = iredapd_greylist.update_greylist_settings_from_form(account='@.', form=form)
if qr[0]:
raise web.seeother('/system/greylisting?msg=GL_UPDATED')
else:
raise web.seeother('/system/greylisting?msg=%s' % web.urlquote(qr[1]))
class GreylistingRawTrackingData:
@decorators.require_domain_access
def GET(self, domain):
(_status, _result) = iredapd_greylist.get_domain_tracking_data(domain=domain)
if not _status:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
return web.render('iredapd/greylisting_tracking_records.html',
domain=domain,
tracking_records=_result)

217
controllers/iredapd/log.py Normal file
View File

@@ -0,0 +1,217 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
from typing import List
import web
import settings
from libs.iredapd import log as iredapd_log, wblist_senderscore
from libs.iredapd import greylist as lib_greylist
if settings.backend == 'ldap':
from libs.ldaplib import decorators
else:
from libs.sqllib import decorators
session = web.config.get('_session')
def _filter_whitelisted_senderscore_ips(rows=None) -> List:
# Get IP addresses of rejected sessions due to senderscore.
whitelisted_ips = []
try:
_rejected_ips = [
row.client_address
for row in rows
if row.action == 'REJECT'
and row.reason.startswith('Server IP address has bad reputation')
]
if _rejected_ips:
_qr = wblist_senderscore.filter_whitelisted_ips(ips=_rejected_ips)
if _qr[0]:
whitelisted_ips = _qr[1]
except:
pass
return whitelisted_ips
def _filter_whitelisted_greylisting_ips(rows=None):
# Get IP addresses of rejected sessions due to greylisting.
whitelisted_ips = []
if not rows:
return whitelisted_ips
try:
_rejected_ips = [
row.client_address
for row in rows
if row.action == '451'
and row.reason == '4.7.1 Intentional policy rejection, please try again later'
]
if _rejected_ips:
_qr = lib_greylist.filter_whitelisted_ips(ips=_rejected_ips)
if _qr[0]:
whitelisted_ips = _qr[1]
except:
pass
return whitelisted_ips
class SMTPSessions:
@decorators.require_admin_login
def GET(self, page=1, outbound_only=False, rejected_only=False):
"""Display log of SMTP rejections."""
page = int(page)
if page < 1:
page = 1
qr = iredapd_log.get_log_smtp_sessions(
outbound_only=outbound_only,
rejected_only=rejected_only,
offset=settings.PAGE_SIZE_LIMIT * (page - 1),
limit=settings.PAGE_SIZE_LIMIT,
)
total = qr['total']
rows = qr['rows']
if outbound_only:
tmpl = 'smtp_outbound_sessions.html'
else:
tmpl = 'smtp_sessions.html'
num_insecure_outbound = 0
insecure_outbound_usernames = []
query_insecure_outbound_hours = settings.IREDAPD_QUERY_INSECURE_OUTBOUND_IN_HOURS
if outbound_only:
# Count insecure outbound connections.
_qr = iredapd_log.get_smtp_insecure_outbound(hours=query_insecure_outbound_hours)
if _qr[0]:
num_insecure_outbound = _qr[1]['total']
insecure_outbound_usernames = _qr[1]['usernames']
# Get IP addresses of rejected sessions due to senderscore.
whitelisted_senderscore_ips = []
if session.get('is_global_admin') and total > 0:
whitelisted_senderscore_ips = _filter_whitelisted_senderscore_ips(rows=rows)
# Get IP addresses of rejected sessions due to greylisting.
whitelisted_greylisting_ips = []
if session.get('is_global_admin') and total > 0:
whitelisted_greylisting_ips = _filter_whitelisted_greylisting_ips(rows=rows)
return web.render('iredapd/activities/' + tmpl,
total=total,
rows=rows,
current_page=page,
rejected_only=rejected_only,
whitelisted_senderscore_ips=whitelisted_senderscore_ips,
whitelisted_greylisting_ips=whitelisted_greylisting_ips,
query_insecure_outbound_hours=query_insecure_outbound_hours,
num_insecure_outbound=num_insecure_outbound,
insecure_outbound_usernames=insecure_outbound_usernames,
msg=web.input().get('msg'))
class SMTPSessionsPerAccount:
@decorators.require_admin_login
def GET(self, account_type, account, page=1, outbound_only=False):
"""Display log of SMTP authentications."""
account_type = account_type.lower()
account = account.lower()
page = int(page)
if page < 1:
page = 1
domains = []
sasl_usernames = []
senders = []
recipients = []
client_addresses = []
encryption_protocols = []
# Make sure admin has privilege to manage this domain.
if account_type == 'sasl_username':
sasl_usernames = [account]
elif account_type == 'sender':
senders = [account]
elif account_type == 'recipient':
recipients = [account]
elif account_type == 'domain':
domains = [account]
elif account_type == 'client_address':
client_addresses = [account]
elif account_type == 'encryption_protocol':
encryption_protocols = [account]
qr = iredapd_log.get_log_smtp_sessions(
domains=domains,
sasl_usernames=sasl_usernames,
senders=senders,
recipients=recipients,
encryption_protocols=encryption_protocols,
client_addresses=client_addresses,
outbound_only=outbound_only,
offset=settings.PAGE_SIZE_LIMIT * (page - 1),
limit=settings.PAGE_SIZE_LIMIT,
)
total = qr['total'] or 0
rows = qr['rows']
if outbound_only:
tmpl = 'smtp_outbound_sessions.html'
else:
tmpl = 'smtp_sessions.html'
# Get IP addresses of rejected sessions due to senderscore.
whitelisted_senderscore_ips = []
if session.get('is_global_admin') and total > 0:
whitelisted_senderscore_ips = _filter_whitelisted_senderscore_ips(rows=rows)
# Get IP addresses of rejected sessions due to greylisting.
whitelisted_greylisting_ips = []
if session.get('is_global_admin') and total > 0:
whitelisted_greylisting_ips = _filter_whitelisted_greylisting_ips(rows=rows)
return web.render(
'iredapd/activities/' + tmpl,
account_type=account_type,
account=account,
total=total,
rows=rows,
whitelisted_senderscore_ips=whitelisted_senderscore_ips,
whitelisted_greylisting_ips=whitelisted_greylisting_ips,
current_page=page,
msg=web.input().get('msg'),
)
class SMTPSessionsRejected:
@decorators.require_admin_login
def GET(self, page=1):
c = SMTPSessions()
return c.GET(page=page, rejected_only=True)
class SMTPSessionsOutbound:
@decorators.require_admin_login
def GET(self, page=1):
c = SMTPSessions()
return c.GET(page=page, outbound_only=True)
class SMTPSessionsOutboundPerAccount:
@decorators.require_admin_login
def GET(self, account_type, account, page=1):
c = SMTPSessionsPerAccount()
return c.GET(account_type=account_type,
account=account,
page=page,
outbound_only=True)

View File

@@ -0,0 +1,14 @@
from controllers import decorators
from controllers.utils import api_render
from libs.iredapd import wblist_senderscore
class WhitelistIPForSenderScore:
@decorators.require_global_admin
def PUT(self, ip):
"""Whitelist given IP address for senderscore.
curl -X PUT -i -b cookie.txt -d "ip=x.x.x.x" https://<server>/api/wblist/senderscore/whitelist/<ip>
"""
qr = wblist_senderscore.whitelist_ips(ips=[ip])
return api_render(qr)

View File

@@ -0,0 +1,45 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs import form_utils
from libs.iredapd import throttle as iredapd_throttle
if settings.backend == 'ldap':
from libs.ldaplib import decorators
else:
from libs.sqllib import decorators
# server-wide throttle setting.
class GlobalThrottle:
@decorators.require_global_admin
def GET(self):
inbound_setting = iredapd_throttle.get_throttle_setting(account='@.', inout_type='inbound')
outbound_setting = iredapd_throttle.get_throttle_setting(account='@.', inout_type='outbound')
return web.render('iredapd/throttle_global.html',
inbound_setting=inbound_setting,
outbound_setting=outbound_setting,
msg=web.input().get('msg'))
@decorators.require_global_admin
def POST(self):
form = web.input(_unicode=False)
t_account = '@.'
inbound_setting = form_utils.get_throttle_setting(form, account=t_account, inout_type='inbound')
outbound_setting = form_utils.get_throttle_setting(form, account=t_account, inout_type='outbound')
iredapd_throttle.add_throttle(account=t_account,
setting=inbound_setting,
inout_type='inbound')
iredapd_throttle.add_throttle(account=t_account,
setting=outbound_setting,
inout_type='outbound')
raise web.seeother('/system/throttle?msg=UPDATED')

View File

@@ -0,0 +1,69 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import settings
from libs.regxes import email as e, domain as d, ip
# fmt: off
urls = [
# Throttling
'/system/throttle', 'controllers.iredapd.throttle.GlobalThrottle',
# Greylisting
'/system/greylisting', 'controllers.iredapd.greylist.DefaultGreylisting',
# Greylisting tracking data
'/system/greylisting/tracking/domain/(%s)' % d, 'controllers.iredapd.greylist.GreylistingRawTrackingData',
# White/blacklist based on rDNS
'/system/wblist/rdns', 'controllers.iredapd.wblist_rdns.WBListRDNS',
#
# Activities
#
'/activities/smtp/sessions', 'controllers.iredapd.log.SMTPSessions',
r'/activities/smtp/sessions/page/(\d+)', 'controllers.iredapd.log.SMTPSessions',
'/activities/smtp/sessions/(sasl_username|sender|recipient)/(%s)' % e, 'controllers.iredapd.log.SMTPSessionsPerAccount',
r'/activities/smtp/sessions/(sasl_username|sender|recipient)/(%s)/page/(\d+)' % e, 'controllers.iredapd.log.SMTPSessionsPerAccount',
'/activities/smtp/sessions/(domain)/(%s)' % d, 'controllers.iredapd.log.SMTPSessionsPerAccount',
r'/activities/smtp/sessions/(domain)/(%s)/page/(\d+)' % d, 'controllers.iredapd.log.SMTPSessionsPerAccount',
'/activities/smtp/sessions/(client_address)/(%s)' % ip, 'controllers.iredapd.log.SMTPSessionsPerAccount',
r'/activities/smtp/sessions/(client_address)/(%s)/page/(\d+)' % ip, 'controllers.iredapd.log.SMTPSessionsPerAccount',
r'/activities/smtp/sessions/(encryption_protocol)/([0-9a-zA-Z\.]+)', 'controllers.iredapd.log.SMTPSessionsPerAccount',
r'/activities/smtp/sessions/(encryption_protocol)/([0-9a-zA-Z\.]+)/page/(\d+)', 'controllers.iredapd.log.SMTPSessionsPerAccount',
'/activities/smtp/sessions/rejected', 'controllers.iredapd.log.SMTPSessionsRejected',
r'/activities/smtp/sessions/rejected/page/(\d+)', 'controllers.iredapd.log.SMTPSessionsRejected',
# SMTP Authentications
'/activities/smtp/sessions/outbound', 'controllers.iredapd.log.SMTPSessionsOutbound',
r'/activities/smtp/sessions/outbound/page/(\d+)', 'controllers.iredapd.log.SMTPSessionsOutbound',
'/activities/smtp/sessions/outbound/(sasl_username|sender|recipient)/(%s)' % e, 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
r'/activities/smtp/sessions/outbound/(sasl_username|sender|recipient)/(%s)/page/(\d+)' % e, 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
'/activities/smtp/sessions/outbound/(domain)/(%s)' % d, 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
r'/activities/smtp/sessions/outbound/(domain)/(%s)/page/(\d+)' % d, 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
'/activities/smtp/sessions/outbound/(client_address)/(%s)' % ip, 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
r'/activities/smtp/sessions/outbound/(client_address)/(%s)/page/(\d+)' % ip, 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
r'/activities/smtp/sessions/outbound/(encryption_protocol)/([0-9a-zA-Z\.]+)', 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
r'/activities/smtp/sessions/outbound/(encryption_protocol)/([0-9a-zA-Z\.]+)/page/(\d+)', 'controllers.iredapd.log.SMTPSessionsOutboundPerAccount',
# API interfaces used by web ui.
'/api/wblist/senderscore/whitelist/(%s)$' % ip, 'controllers.iredapd.senderscore.WhitelistIPForSenderScore',
'/api/greylisting/global/whitelist/(%s)$' % ip, 'controllers.iredapd.api_greylist.APIGlobalWhitelist',
]
# API Interfaces
if settings.ENABLE_RESTFUL_API:
urls += [
# Throttling
'/api/throttle/global/(inbound|outbound)', 'controllers.iredapd.api_throttle.APIGlobalThrottle',
'/api/throttle/(%s)/(inbound|outbound)' % d, 'controllers.iredapd.api_throttle.APIDomainThrottle',
'/api/throttle/(%s)/(inbound|outbound)' % e, 'controllers.iredapd.api_throttle.APIUserThrottle',
# Greylisting
'/api/greylisting/all', 'controllers.iredapd.api_greylist.APIAllSettings',
'/api/greylisting/global', 'controllers.iredapd.api_greylist.APIGlobalSetting',
'/api/greylisting/(%s)' % d, 'controllers.iredapd.api_greylist.APIDomainSetting',
'/api/greylisting/(%s)' % e, 'controllers.iredapd.api_greylist.APIUserSetting',
'/api/greylisting/global/whitelists', 'controllers.iredapd.api_greylist.APIGlobalWhitelists',
'/api/greylisting/(%s)/whitelists' % d, 'controllers.iredapd.api_greylist.APIDomainWhitelists',
'/api/greylisting/(%s)/whitelists' % e, 'controllers.iredapd.api_greylist.APIUserWhitelists',
'/api/greylisting/whitelist_spf_domains', 'controllers.iredapd.api_greylist.APIWhitelistSPFDomain',
]
# fmt: on

View File

@@ -0,0 +1,79 @@
import web
from controllers import decorators
from libs.iredutils import is_valid_wblist_rdns_domain
from libs.iredapd import wblist_rdns, wblist_senderscore
class WBListRDNS:
@decorators.require_global_admin
def GET(self):
# Get wblist records
(_status, _result) = wblist_rdns.get_wblist_rdns()
if not _status:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
whitelists = _result['whitelists']
blacklists = _result['blacklists']
return web.render('iredapd/wblist/rdns.html',
whitelists=whitelists,
blacklists=blacklists,
msg=web.input().get('msg'))
@decorators.require_global_admin
def POST(self):
form = web.input()
whitelists = [str(i).lower()
for i in form.get('whitelists', '').splitlines()
if is_valid_wblist_rdns_domain(i)]
whitelists = list(set(whitelists))
blacklists = [str(i).lower()
for i in form.get('blacklists', '').splitlines()
if is_valid_wblist_rdns_domain(i)]
blacklists = list(set(blacklists))
(_status, _result) = wblist_rdns.reset_wblist_rdns(whitelists=whitelists, blacklists=blacklists)
if _status:
raise web.seeother('/system/wblist/rdns?msg=UPDATED')
else:
raise web.seeother('/system/wblist/rdns?msg=%s' % web.urlquote(_result))
class WBListSenderScore:
@decorators.require_global_admin
def GET(self):
# Get wblist records
(_status, _result) = wblist_senderscore.get_whitelists()
if not _status:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
total = _result['total']
ips = _result['ips']
return web.render('iredapd/wblist/senderscore.html',
total=total,
ips=ips,
msg=web.input().get('msg'))
@decorators.require_global_admin
def POST(self):
form = web.input()
whitelists = [str(i).lower()
for i in form.get('whitelists', '').splitlines()
if is_valid_wblist_rdns_domain(i)]
whitelists = list(set(whitelists))
blacklists = [str(i).lower()
for i in form.get('blacklists', '').splitlines()
if is_valid_wblist_rdns_domain(i)]
blacklists = list(set(blacklists))
(_status, _result) = wblist_rdns.reset_wblist_rdns(whitelists=whitelists, blacklists=blacklists)
if _status:
raise web.seeother('/system/wblist/senderscore?msg=UPDATED')
else:
raise web.seeother('/system/wblist/senderscore?msg=%s' % web.urlquote(_result))

View File

View File

@@ -0,0 +1,387 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import time
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
import web
from libs import iredutils
from libs.logger import logger
from libs.mlmmj import add_subscribers, remove_subscribers
import settings
if settings.backend == 'ldap':
from libs.ldaplib.ml import get_profile_by_mlid
else:
from libs.sqllib.ml import get_profile_by_mlid
base_url = web.ctx.homedomain + settings.NEWSLETTER_BASE_URL
class Error:
"""Display error messages happened during subscription/unsubscription."""
def GET(self):
form = web.input(_unicode=False)
msg = form.get('msg')
return web.render('mlmmj/errors.html', msg=msg)
# SubUnsubSSR returns HTML snippet to requester directly.
class SubUnsubSSR:
def OPTIONS(self, action, mlid):
# These headers are used when HTTP POST requests are sent from web page
# running on another domain.
web.header("Access-Control-Allow-Origin", "*")
web.header("Access-Control-Allow-Headers", "*")
web.header("Access-Control-Allow-Methods", "POST")
return ""
def POST(self, action, mlid):
web.header("Access-Control-Allow-Origin", "*")
if action not in ['subscribe']:
return "INVALID_ACTION"
form = web.input(_unicode=False)
subscriber = form.get('subscriber', '').lower()
if not iredutils.is_email(subscriber):
return "Invalid email address."
# Get newsletter profile
qr = get_profile_by_mlid(mlid=mlid)
if not qr[0]:
return "Invalid newsletter."
profile = qr[1]
if settings.backend == 'ldap':
mail = profile['mail'][0]
name = profile.get('cn', [''])[0]
else:
mail = profile['address']
name = profile['name']
# Generate an unique string as verification token
token = iredutils.generate_random_strings(length=32)
# Set expire date for this subscription request
if action == 'subscribe':
_expire_hours = settings.NEWSLETTER_SUBSCRIPTION_REQUEST_EXPIRE_HOURS
else:
_expire_hours = settings.NEWSLETTER_UNSUBSCRIPTION_REQUEST_EXPIRE_HOURS
expire_date = int(time.time()) + (int(_expire_hours) * 60 * 60)
#
# Store this subscription request in sql db.
#
try:
# Delete existing subscription confirm.
web.conn_iredadmin.delete(
'newsletter_subunsub_confirms',
vars={
'mlid': mlid,
'subscriber': subscriber,
'kind': action,
},
where='mlid=$mlid AND subscriber=$subscriber AND kind=$kind',
)
# Insert a new record
web.conn_iredadmin.insert(
'newsletter_subunsub_confirms',
mail=mail,
mlid=mlid,
subscriber=subscriber,
kind=action,
token=token,
expired=expire_date,
)
except Exception as e:
logger.error(e)
return "Internal server error, please try again later."
#
# Send confirm email
#
# Generate mail message
_msg = MIMEMultipart('alternative')
# Set mailing list address as sender in `From:`
_smtp_sender = mail
_smtp_sender_name = settings.NOTIFICATION_SENDER_NAME
if _smtp_sender_name:
_msg['From'] = '{} <{}>'.format(Header(_smtp_sender_name, 'utf-8'), _smtp_sender)
else:
_msg['From'] = _smtp_sender
_msg['To'] = subscriber
if action == 'subscribe':
_msg_subject = 'Subscription confirm'
_subunsub_url = base_url + '/subconfirm/{}/{}'.format(mlid, token)
else:
_msg_subject = 'Unsubscription confirm'
_subunsub_url = base_url + '/unsubconfirm/{}/{}'.format(mlid, token)
# Add mailing list name.
if name:
_msg_subject += ': ' + name
_msg['Subject'] = Header(_msg_subject, 'utf-8')
if action == 'subscribe':
_msg_body = 'Please click link below to confirm subscription to newsletter'
else:
_msg_body = 'Please click link below to confirm unsubscription from newsletter'
if name:
_msg_body += ' "' + name + '"'
_msg_body += ':\n' + _subunsub_url + '\n'
_msg_body += '\nLink will expire in %d hours.' % settings.NEWSLETTER_SUBSCRIPTION_REQUEST_EXPIRE_HOURS
_msg_body += '\nIf this is not requested by you, please simply ignore this email.'
_msg_body_plain = MIMEText(_msg_body, 'plain', 'utf-8')
_msg.attach(_msg_body_plain)
_msg_string = _msg.as_string()
qr = iredutils.sendmail(
recipients=subscriber,
message_text=_msg_string,
from_address=_smtp_sender,
)
if qr[0]:
if action == 'subscribe':
return "Almost done, an email has been sent to the address, please click the link in email to confirm the subscription."
else:
return "Almost done, an email has been sent to the address, please click the link in email to unsubscribe."
else:
return qr[1]
class SubUnsub:
"""Handle the subscription and unsubscription."""
def GET(self, action, mlid):
if action not in ['subscribe', 'unsubscribe']:
raise web.seeother(base_url + '/error?msg=INVALID_ACTION', absolute=True)
# Display a subscription form.
form = web.input(_unicode=False)
msg = form.get('msg')
# Get newsletter profile
qr = get_profile_by_mlid(mlid=mlid)
if not qr[0]:
raise web.seeother(base_url + '/error?msg=INVALID_NEWSLETTER', absolute=True)
profile = qr[1]
# Get display name and description
if settings.backend == 'ldap':
name = profile.get('cn', [''])[0]
description = profile.get('description', [''])[0]
else:
name = profile['name']
description = profile['description']
# Get basic newsletter info: display name, short introduction.
return web.render('mlmmj/subunsub.html',
action=action,
mlid=mlid,
name=name,
description=description,
msg=msg)
def POST(self, action, mlid):
if action not in ['subscribe', 'unsubscribe']:
raise web.seeother(base_url + '/error?msg=INVALID_ACTION', absolute=True)
form = web.input(_unicode=False)
subscriber = form.get('subscriber', '').lower()
if not iredutils.is_email(subscriber):
raise web.seeother(base_url + '/error?msg=INVALID_SUBSCRIBER_EMAIL_ADDRESS', absolute=True)
# Get newsletter profile
qr = get_profile_by_mlid(mlid=mlid)
if not qr[0]:
raise web.seeother(base_url + '/error?msg=INVALID_NEWSLETTER', absolute=True)
profile = qr[1]
if settings.backend == 'ldap':
mail = profile['mail'][0]
name = profile.get('cn', [''])[0]
else:
mail = profile['address']
name = profile['name']
# Generate an unique string as verification token
token = iredutils.generate_random_strings(length=32)
# Set expire date for this subscription request
if action == 'subscribe':
_expire_hours = settings.NEWSLETTER_SUBSCRIPTION_REQUEST_EXPIRE_HOURS
else:
_expire_hours = settings.NEWSLETTER_UNSUBSCRIPTION_REQUEST_EXPIRE_HOURS
expire_date = int(time.time()) + (int(_expire_hours) * 60 * 60)
#
# Store this subscription request in sql db.
#
try:
# Delete existing subscription confirm.
web.conn_iredadmin.delete(
'newsletter_subunsub_confirms',
vars={'mlid': mlid, 'subscriber': subscriber, 'kind': action},
where='mlid=$mlid AND subscriber=$subscriber AND kind=$kind',
)
# Insert a new record
web.conn_iredadmin.insert(
'newsletter_subunsub_confirms',
mail=mail,
mlid=mlid,
subscriber=subscriber,
kind=action,
token=token,
expired=expire_date,
)
except Exception as e:
logger.error(e)
raise web.seeother(base_url + '/error?msg=INTERNAL_SERVER_ERROR', absolute=True)
#
# Send confirm email
#
# Generate mail message
_msg = MIMEMultipart('alternative')
# Set mailing list address as sender in `From:`
_smtp_sender = mail
_smtp_sender_name = settings.NOTIFICATION_SENDER_NAME
if _smtp_sender_name:
_msg['From'] = '{} <{}>'.format(Header(_smtp_sender_name, 'utf-8'), _smtp_sender)
else:
_msg['From'] = _smtp_sender
_msg['To'] = subscriber
if action == 'subscribe':
_msg_subject = 'Subscription confirm'
_subunsub_url = base_url + '/subconfirm/{}/{}'.format(mlid, token)
else:
_msg_subject = 'Unsubscription confirm'
_subunsub_url = base_url + '/unsubconfirm/{}/{}'.format(mlid, token)
# Add mailing list name.
if name:
_msg_subject += ': ' + name
_msg['Subject'] = Header(_msg_subject, 'utf-8')
if action == 'subscribe':
_msg_body = 'Please click link below to confirm subscription to newsletter'
else:
_msg_body = 'Please click link below to confirm unsubscription from newsletter'
if name:
_msg_body += ' "' + name + '"'
_msg_body += ':\n' + _subunsub_url + '\n'
_msg_body += '\nLink will expire in %d hours.' % settings.NEWSLETTER_SUBSCRIPTION_REQUEST_EXPIRE_HOURS
_msg_body += '\nIf this is not requested by you, please simply ignore this email.'
_msg_body_plain = MIMEText(_msg_body, 'plain', 'utf-8')
_msg.attach(_msg_body_plain)
_msg_string = _msg.as_string()
qr = iredutils.sendmail(
recipients=subscriber,
message_text=_msg_string,
from_address=_smtp_sender,
)
if qr[0]:
if action == 'subscribe':
raise web.seeother(base_url + '/subscribe/%s?msg=WAIT_FOR_SUBCONFIRM' % mlid, absolute=True)
else:
raise web.seeother(base_url + '/unsubscribe/%s?msg=WAIT_FOR_UNSUBCONFIRM' % mlid, absolute=True)
else:
raise web.seeother(base_url + '/error?msg=%s' % web.urlquote(qr[1]), absolute=True)
class SubUnsubConfirm:
"""Process subscription confirm."""
def GET(self, action, mlid, token):
if action == 'subconfirm':
action = 'subscribe'
elif action == 'unsubconfirm':
action = 'unsubscribe'
else:
raise web.seeother(base_url + '/error?msg=INVALID_ACTION', absolute=True)
if not iredutils.is_mlid(mlid):
raise web.seeother(base_url + '/error?msg=INVALID_NEWSLETTER', absolute=True)
if not iredutils.is_ml_confirm_token(token):
raise web.seeother(base_url + '/error?msg=TOKEN_INVALID', absolute=True)
_record = {}
try:
now = int(time.time())
qr = web.conn_iredadmin.select(
'newsletter_subunsub_confirms',
vars={'mlid': mlid, 'token': token, 'kind': action, 'now': now},
what='mail, mlid, subscriber',
where='mlid=$mlid AND token=$token AND kind=$kind AND expired >= $now',
limit=1,
)
qr = list(qr)
if qr:
_record = qr[0]
except Exception as e:
raise web.seeother(base_url + '/error?msg=%s' % web.urlquote(repr(e)), absolute=True)
if not _record:
raise web.seeother(base_url + '/error?msg=TOKEN_EXPIRED', absolute=True)
_mail = str(_record['mail']).lower()
_subscriber = str(_record['subscriber']).lower()
# Subscribe this subscriber
if action == 'subscribe':
qr = add_subscribers(mail=_mail,
subscribers=[_subscriber],
require_confirm=False)
else:
qr = remove_subscribers(mail=_mail, subscribers=[_subscriber])
if not qr[0]:
raise web.seeother(base_url + '/error?msg=%s' % web.urlquote(qr[1]), absolute=True)
try:
# Update the record expire time, instead of deleting the record.
now = int(time.time())
web.conn_iredadmin.update(
'newsletter_subunsub_confirms',
vars={'mlid': mlid, 'token': token, 'kind': action},
expired=now,
where='mlid=$mlid AND token=$token AND kind=$kind',
)
except Exception as e:
raise web.seeother(base_url + '/error?msg=%s' % web.urlquote(repr(e)), absolute=True)
if action == 'subscribe':
raise web.seeother(base_url + '/subscribe/%s?msg=SUBSCRIBED' % mlid, absolute=True)
else:
raise web.seeother(base_url + '/unsubscribe/%s?msg=UNSUBSCRIBED' % mlid, absolute=True)

13
controllers/mlmmj/urls.py Normal file
View File

@@ -0,0 +1,13 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
from libs.regxes import mailing_list_id as mlid
from libs.regxes import mailing_list_confirm_token as confirm_token
# fmt: off
urls = [
'/newsletter/noninteractive/(subscribe)/(%s)$' % mlid, 'controllers.mlmmj.newsletter.SubUnsubSSR',
'/newsletter/(subscribe|unsubscribe)/(%s)$' % mlid, 'controllers.mlmmj.newsletter.SubUnsub',
'/newsletter/(subconfirm|unsubconfirm)/({})/({})$'.format(mlid, confirm_token), 'controllers.mlmmj.newsletter.SubUnsubConfirm',
# Handle error messages
'/newsletter/error', 'controllers.mlmmj.newsletter.Error',
]
# fmt: on

View File

View File

@@ -0,0 +1,69 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from controllers import decorators
from libs import iredutils
from libs.panel.domain_ownership import get_pending_domains, verify_domain_ownership
if settings.backend == 'ldap':
from libs.ldaplib.domain import update_ownership_verified_domain
from libs.ldaplib.domain import enable_domain_without_ownership_verification
else:
from libs.sqllib.domain import update_ownership_verified_domain
from libs.sqllib.domain import enable_domain_without_ownership_verification
session = web.config.get('_session', {})
class VerifyOwnership:
@decorators.require_admin_login
def GET(self):
qr = get_pending_domains()
if qr[0]:
ownership_verify_codes = qr[1]
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
return web.render('panel/domain_ownership.html',
ownership_verify_codes=ownership_verify_codes,
msg=web.input().get('msg', ''))
@decorators.require_admin_login
def POST(self):
form = web.input(domain=[])
if 'verify' in form:
action = 'verify'
elif 'enable_without_verification' in form:
action = 'enable_without_verification'
else:
raise web.seeother('/verify/domain_ownership?msg=INVALID_ACTION')
domains = form.get('domain', [])
domains = [str(d).lower() for d in domains if iredutils.is_domain(d)]
if action == 'verify':
_qr = verify_domain_ownership(domains=domains)
if _qr[0]:
verified_domains = _qr[1]
for (pd, ad) in verified_domains:
qr = update_ownership_verified_domain(primary_domain=pd,
alias_domain=ad)
if not qr[0]:
raise web.seeother('/verify/domain_ownership?msg=%s' % web.urlquote(qr[1]))
raise web.seeother('/verify/domain_ownership')
else:
raise web.seeother('/verify/domain_ownership?msg=%s' % web.urlquote(_qr[1]))
elif action == 'enable_without_verification':
# Enable domains, and mark them as verified
if not session.get('is_global_admin'):
raise web.seeother('/verify/domain_ownership?msg=PERMISSION_DENIED')
qr = enable_domain_without_ownership_verification(domains=domains)
if not qr[0]:
raise web.seeother('/verify/domain_ownership?msg=%s' % web.urlquote(qr[1]))
raise web.seeother('/verify/domain_ownership')

180
controllers/panel/log.py Normal file
View File

@@ -0,0 +1,180 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs import __url_license_terms__
from libs import sysinfo
from controllers import decorators
from libs.panel import LOG_EVENTS, log as loglib
session = web.config.get('_session')
if settings.backend == 'ldap':
from libs.ldaplib.core import LDAPWrap
from libs.ldaplib import admin as ldap_lib_admin
from libs import __version_ldap__ as __version__
elif settings.backend in ['mysql', 'pgsql']:
from libs import __version_sql__ as __version__
from libs.sqllib import SQLWrap, admin as sql_lib_admin
class Log:
@decorators.require_admin_login
def GET(self):
form = web.input(_unicode=False)
# Get queries.
form_event = web.safestr(form.get('event', 'all'))
form_domain = web.safestr(form.get('domain', 'all'))
form_admin = web.safestr(form.get('admin', 'all'))
form_cur_page = web.safestr(form.get('page', '1'))
if not form_cur_page.isdigit() or form_cur_page == '0':
form_cur_page = 1
else:
form_cur_page = int(form_cur_page)
total, entries = loglib.list_logs(event=form_event,
domain=form_domain,
admin=form_admin,
cur_page=form_cur_page)
# Pre-defined
all_domain_names = []
all_admin_emails = []
if settings.backend == 'ldap':
_wrap = LDAPWrap()
conn = _wrap.conn
# Get all managed domains under control.
qr = ldap_lib_admin.get_managed_domains(
admin=session.get('username'),
domain_name_only=True,
conn=conn,
)
if qr[0]:
all_domain_names = qr[1]
# Get all admins.
if session.get('is_global_admin'):
result = ldap_lib_admin.list_accounts(attributes=['mail'], conn=conn)
if result[0] is not False:
all_admin_emails = [v[1]['mail'][0] for v in result[1]]
else:
all_admin_emails = [form_admin]
elif settings.backend in ['mysql', 'pgsql']:
# Get all managed domains under control.
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_admin.get_managed_domains(
admin=session.get('username'),
domain_name_only=True,
conn=conn,
)
if qr[0]:
all_domain_names = qr[1]
# Get all admins.
if session.get('is_global_admin'):
qr = sql_lib_admin.get_all_admins(columns=['username'], email_only=True, conn=conn)
if qr[0]:
all_admin_emails = qr[1]
else:
all_admin_emails = [form_admin]
all_domain_names.sort()
all_admin_emails.sort()
return web.render('panel/log.html',
event=form_event,
domain=form_domain,
admin=form_admin,
log_events=LOG_EVENTS,
cur_page=form_cur_page,
total=total,
entries=entries,
all_domain_names=all_domain_names,
all_admin_emails=all_admin_emails,
msg=form.get('msg'))
@decorators.require_global_admin
@decorators.csrf_protected
@decorators.require_admin_login
def POST(self):
form = web.input(_unicode=False, id=[])
action = form.get('action', 'delete')
delete_all = False
if action == 'deleteAll':
delete_all = True
qr = loglib.delete_logs(form=form, delete_all=delete_all)
if qr[0]:
# Keep the log filter.
form_domain = web.safestr(form.get('domain'))
form_admin = web.safestr(form.get('admin'))
form_event = web.safestr(form.get('event'))
url = 'domain={}&admin={}&event={}'.format(form_domain, form_admin, form_event)
raise web.seeother('/activities/admins?%s&msg=DELETED' % url)
else:
raise web.seeother('/activities/admins?msg=%s' % web.urlquote(qr[1]))
class License:
@decorators.require_global_admin
def GET(self):
qr_info = sysinfo.get_license_info()
if qr_info[0]:
latest_ver = qr_info[1].get('latestversion', '1.0')
has_update = False
try:
# Convert version number to major + minor numbers, then
# convert to integer and compare.
#
# Warning: Comparing (float) numbers in string format is not
# accurate. For example, version "4.10" is "older" than "4.9".
latest_vers = latest_ver.split(".", 1)
if len(latest_vers) == 2:
latest_major = latest_vers[0]
latest_minor = latest_vers[1]
else:
latest_major = latest_ver
latest_minor = "0"
cur_vers = __version__.split(".", 1)
if len(cur_vers) == 2:
cur_major = cur_vers[0]
cur_minor = cur_vers[1]
else:
cur_major = __version__
cur_minor = "0"
# Convert to int.
i_latest_major = int(latest_major)
i_latest_minor = int(latest_minor)
i_cur_major = int(cur_major)
i_cur_minor = int(cur_minor)
if i_latest_major > i_cur_major:
has_update = True
if (i_latest_major == i_cur_major) and (i_latest_minor > i_cur_minor):
has_update = True
if has_update:
session['new_version_available'] = True
session['new_version'] = latest_ver
except:
pass
return web.render('panel/license.html',
info=qr_info[1],
url_license_terms=__url_license_terms__,
version=__version__)
else:
return web.render('panel/license.html', error=qr_info[1])

View File

@@ -0,0 +1,36 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from controllers import decorators
from libs import iredutils, form_utils
class Settings:
@decorators.require_global_admin
def GET(self):
db_settings = iredutils.get_settings_from_db(account='global')
return web.render('panel/settings.html',
db_settings=db_settings)
@decorators.require_global_admin
@decorators.csrf_protected
def POST(self):
form = web.input()
# Re-format value of some parameters, then replace the value in `form`.
# input: textarea
for k in ['global_admin_ip_list',
'admin_login_ip_list',
'restful_api_clients']:
_list = form_utils.get_multi_values(form=form,
input_name=k,
input_is_textarea=True,
is_ip_or_network=True)
form[k] = _list
qr = iredutils.store_settings_in_db(kvs=form, flush=True)
if qr[0]:
return web.seeother('/system/settings?msg=UPDATED')
else:
return web.seeother('/system/settings?msg=' + web.urlquote(qr[1]))

12
controllers/panel/urls.py Normal file
View File

@@ -0,0 +1,12 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
# fmt: off
urls = [
'/expired', 'controllers.utils.Expired',
'/system', 'controllers.panel.log.Log',
'/system/settings', 'controllers.panel.sys_settings.Settings',
'/system/license', 'controllers.panel.log.License',
'/activities/admins', 'controllers.panel.log.Log',
'/verify/domain_ownership', 'controllers.panel.domain_ownership.VerifyOwnership',
]
# fmt: on

View File

208
controllers/sql/admin.py Normal file
View File

@@ -0,0 +1,208 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs import iredutils
from libs.l10n import TIMEZONES
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import general as sql_lib_general
from libs.sqllib import user as sql_lib_user
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import utils as sql_lib_utils
session = web.config.get('_session')
class List:
@decorators.require_global_admin
def GET(self, cur_page=1):
form = web.input()
cur_page = int(cur_page)
if cur_page == 0:
cur_page = 1
_wrap = SQLWrap()
conn = _wrap.conn
result = sql_lib_admin.get_paged_admins(conn=conn,
cur_page=cur_page)
if result[0]:
(total, records) = (result[1]['total'], result[1]['records'])
# Get list of global admins.
all_global_admins = []
qr = sql_lib_admin.get_all_global_admins(conn=conn)
if qr[0]:
all_global_admins = qr[1]
return web.render(
'sql/admin/list.html',
cur_page=cur_page,
total=total,
admins=records,
allGlobalAdmins=all_global_admins,
msg=form.get('msg', None),
)
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(result[1]))
@decorators.require_global_admin
@decorators.csrf_protected
def POST(self):
form = web.input(_unicode=False, mail=[])
accounts = form.get('mail', [])
action = form.get('action', None)
msg = form.get('msg', None)
_wrap = SQLWrap()
conn = _wrap.conn
if action == 'delete':
result = sql_lib_admin.delete_admins(mails=accounts,
revoke_admin_privilege_from_user=True,
conn=conn)
msg = 'DELETED'
elif action == 'disable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type='admin',
enable_account=False)
msg = 'DISABLED'
elif action == 'enable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type='admin',
enable_account=True)
msg = 'ENABLED'
else:
result = (False, 'INVALID_ACTION')
if result[0]:
raise web.seeother('/admins?msg=%s' % msg)
else:
raise web.seeother('/admins?msg=?' + web.urlquote(result[1]))
class Profile:
@decorators.require_admin_login
def GET(self, profile_type, mail):
mail = str(mail).lower()
form = web.input()
if not (session.get('is_global_admin') or session.get('username') == mail):
# Don't allow to view/update others' profile.
raise web.seeother('/profile/admin/general/%s?msg=PERMISSION_DENIED' % session.get('username'))
_wrap = SQLWrap()
conn = _wrap.conn
is_global_admin = sql_lib_general.is_global_admin(admin=mail, conn=conn)
result = sql_lib_admin.get_profile(mail=mail, conn=conn)
if result[0]:
profile = result[1]
qr = sql_lib_general.get_admin_settings(admin=mail, conn=conn)
if qr[0]:
admin_settings = qr[1]
else:
return qr
# Get all domains.
all_domains = []
qr_all_domains = sql_lib_domain.get_all_domains(conn=conn)
if qr_all_domains[0]:
all_domains = qr_all_domains[1]
# Get managed domains.
managed_domains = []
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=mail,
domain_name_only=True,
listed_only=True)
if qr[0]:
managed_domains += qr[1]
return web.render(
'sql/admin/profile.html',
mail=mail,
profile_type=profile_type,
is_global_admin=is_global_admin,
profile=profile,
admin_settings=admin_settings,
languagemaps=iredutils.get_language_maps(),
timezones=TIMEZONES,
allDomains=all_domains,
managedDomains=managed_domains,
min_passwd_length=settings.min_passwd_length,
max_passwd_length=settings.max_passwd_length,
store_password_in_plain_text=settings.STORE_PASSWORD_IN_PLAIN_TEXT,
password_policies=iredutils.get_password_policies(),
msg=form.get('msg'),
)
else:
# Return to user profile page if admin is a mail user.
qr = sql_lib_user.simple_profile(conn=conn,
mail=mail,
columns=['username'])
if qr[0]:
raise web.seeother('/profile/user/general/' + mail)
else:
raise web.seeother('/admins?msg=' + web.urlquote(result[1]))
@decorators.csrf_protected
@decorators.require_admin_login
def POST(self, profile_type, mail):
mail = str(mail).lower()
form = web.input(domainName=[])
if not (session.get('is_global_admin') or session.get('username') == mail):
# Don't allow to view/update others' profile.
raise web.seeother('/profile/admin/general/%s?msg=PERMISSION_DENIED' % session.get('username'))
_wrap = SQLWrap()
conn = _wrap.conn
result = sql_lib_admin.update(mail=mail,
profile_type=profile_type,
form=form,
conn=conn)
if result[0]:
raise web.seeother('/profile/admin/{}/{}?msg=UPDATED'.format(profile_type, mail))
else:
raise web.seeother('/profile/admin/{}/{}?msg={}'.format(profile_type, mail, web.urlquote(result[1])))
class Create:
@decorators.require_global_admin
def GET(self):
form = web.input()
return web.render('sql/admin/create.html',
languagemaps=iredutils.get_language_maps(),
default_language=settings.default_language,
min_passwd_length=settings.min_passwd_length,
max_passwd_length=settings.max_passwd_length,
password_policies=iredutils.get_password_policies(),
msg=form.get('msg'))
@decorators.require_global_admin
@decorators.csrf_protected
def POST(self):
form = web.input()
mail = web.safestr(form.get('mail')).lower()
qr = sql_lib_admin.add_admin_from_form(form=form, conn=None)
if qr[0]:
# Redirect to assign domains.
raise web.seeother('/profile/admin/general/%s?msg=CREATED' % mail)
else:
raise web.seeother('/create/admin?msg=' + web.urlquote(qr[1]))

224
controllers/sql/alias.py Normal file
View File

@@ -0,0 +1,224 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from libs import iredutils, form_utils
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import alias as sql_lib_alias
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import general as sql_lib_general
from libs.sqllib import utils as sql_lib_utils
session = web.config.get('_session')
class List:
@decorators.require_domain_access
def GET(self, domain, cur_page=1, disabled_only=False):
domain = str(domain).lower()
cur_page = int(cur_page) or 1
form = web.input(_unicode=False)
all_first_chars = []
first_char = None
if 'starts_with' in form:
first_char = form.get('starts_with')[:1].upper()
if not iredutils.is_valid_account_first_char(first_char):
first_char = None
_wrap = SQLWrap()
conn = _wrap.conn
total = sql_lib_alias.num_aliases_under_domain(conn=conn,
domain=domain,
disabled_only=disabled_only,
first_char=first_char)
records = []
if total:
_qr = sql_lib_general.get_first_char_of_all_accounts(domain=domain,
account_type='alias',
conn=conn)
if _qr[0]:
all_first_chars = _qr[1]
qr = sql_lib_alias.get_basic_alias_profiles(conn=conn,
domain=domain,
page=cur_page,
first_char=first_char,
disabled_only=disabled_only)
if qr[0]:
records = qr[1]
return web.render(
'sql/alias/list.html',
cur_domain=domain,
cur_page=cur_page,
total=total,
aliases=records,
all_first_chars=all_first_chars,
first_char=first_char,
disabled_only=disabled_only,
msg=form.get('msg', None),
)
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, domain):
form = web.input(_unicode=False, mail=[])
domain = str(domain).lower()
accounts = form.get('mail', [])
action = form.get('action', None)
msg = form.get('msg', None)
# Filter aliases not under the same domain.
accounts = [str(v).lower()
for v in accounts
if iredutils.is_email(v) and str(v).endswith('@' + domain)]
_wrap = SQLWrap()
conn = _wrap.conn
if action == 'delete':
result = sql_lib_alias.delete_aliases(conn=conn,
accounts=accounts)
msg = 'DELETED'
elif action == 'disable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type='alias',
enable_account=False)
msg = 'DISABLED'
elif action == 'enable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type='alias',
enable_account=True)
msg = 'ENABLED'
else:
result = (False, 'INVALID_ACTION')
if result[0]:
raise web.seeother('/aliases/{}?msg={}'.format(domain, msg))
else:
raise web.seeother('/aliases/{}?msg={}'.format(domain, web.urlquote(result[1])))
class ListDisabled:
@decorators.require_domain_access
def GET(self, domain, cur_page=1):
_list = List()
return _list.GET(domain=domain, cur_page=cur_page, disabled_only=True)
class Create:
@decorators.require_domain_access
def GET(self, domain):
domain = str(domain).lower()
form = web.input()
all_domains = []
# Get all domains, select the first one.
_wrap = SQLWrap()
conn = _wrap.conn
if session.get('is_global_admin'):
qr = sql_lib_domain.get_all_domains(conn=conn, name_only=True)
else:
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.get('username'),
domain_name_only=True)
if qr[0]:
all_domains = qr[1]
# Get domain profile.
qr_profile = sql_lib_domain.simple_profile(domain=domain, conn=conn)
if qr_profile[0]:
domain_profile = qr_profile[1]
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr_profile[1]))
# Cet total number and allocated quota size of existing users under domain.
num_aliases_under_domain = sql_lib_alias.num_aliases_under_domain(conn=conn, domain=domain)
return web.render(
'sql/alias/create.html',
cur_domain=domain,
allDomains=all_domains,
profile=domain_profile,
num_existing_aliases=num_aliases_under_domain,
msg=form.get('msg'),
)
@decorators.require_domain_access
@decorators.csrf_protected
def POST(self, domain):
domain = str(domain).lower()
form = web.input()
domain_in_form = form_utils.get_domain_name(form)
if domain != domain_in_form:
raise web.seeother('/domains?msg=PERMISSION_DENIED')
listname = form_utils.get_single_value(form, input_name='listname', to_string=True)
mail = listname + '@' + domain
result = sql_lib_alias.add_alias_from_form(domain=domain, form=form)
if result[0]:
raise web.seeother('/profile/alias/general/%s?msg=CREATED' % mail)
else:
raise web.seeother('/create/alias/{}?msg={}'.format(domain, web.urlquote(result[1])))
class Profile:
@decorators.require_domain_access
def GET(self, profile_type, mail):
if profile_type == 'rename':
raise web.seeother('/profile/alias/general/' + mail)
form = web.input()
mail = web.safestr(mail).lower()
domain = mail.split('@', 1)[-1]
if not iredutils.is_email(mail):
raise web.seeother('/domains?msg=INVALID_MAIL')
qr = sql_lib_alias.get_profile(mail=mail,
with_members=True,
with_moderators=True,
conn=None)
if qr[0]:
profile = qr[1]
else:
raise web.seeother('/aliases/{}?msg={}'.format(domain, web.urlquote(qr[1])))
return web.render('sql/alias/profile.html',
cur_domain=domain,
mail=mail,
profile_type=profile_type,
profile=profile,
msg=form.get('msg'))
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, profile_type, mail):
form = web.input()
result = sql_lib_alias.update(mail=mail,
profile_type=profile_type,
form=form)
if profile_type == 'rename':
profile_type = 'general'
if result[0]:
raise web.seeother('/profile/alias/{}/{}?msg=UPDATED'.format(profile_type, mail))
else:
raise web.seeother('/profile/alias/{}/{}?msg={}'.format(profile_type, mail, web.urlquote(result[1])))

View File

@@ -0,0 +1,158 @@
import web
from controllers.utils import api_render
from libs.sqllib import SQLWrap
from libs.sqllib import decorators, api_utils
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import general as sql_lib_general
import settings
# Parameter names used in API interface and web form, both POST and PUT.
_param_maps = [('maxDomains', 'create_max_domains'),
('maxUsers', 'create_max_users'),
('maxAliases', 'create_max_aliases'),
('maxLists', 'create_max_lists'),
('maxQuota', 'create_max_quota'),
('quotaUnit', 'create_quota_unit')]
class APIAdmin:
@decorators.api_require_global_admin
def GET(self, mail):
"""Get profile of a standalone domain admin.
curl -X GET -i -b cookie.txt https://<server>/api/admin/<mail>
"""
mail = str(mail).lower()
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_admin.get_profile(mail=mail, conn=conn)
if qr[0]:
profile = api_utils.export_sql_record(record=qr[1],
remove_columns=settings.API_HIDDEN_ADMIN_PROFILES)
profile['isglobaladmin'] = 0
if sql_lib_general.is_global_admin(admin=mail, conn=conn):
profile['isglobaladmin'] = 1
_qr = sql_lib_admin.get_managed_domains(admin=mail,
domain_name_only=True,
listed_only=True,
conn=conn)
if _qr[0]:
profile['managed_domains'] = _qr[1]
return api_render((True, profile))
else:
return api_render(qr)
@decorators.api_require_global_admin
def POST(self, mail):
"""Create a new domain.
curl -X POST -i -b cookie.txt -d "var=<value>&var2=value2" https://<server>/api/admin/<mail>
:param mail: admin email address.
Form parameters:
`name`: the display name of this admin
`password`: admin's password
`accountStatus`: account status (active, disabled)
`domainGlobalAdmin`: Mark this admin as global admin (yes, no).
`language`: default preferred language for new user.
e.g. en_US for English, de_DE for Deutsch.
Form parameters listed below are used by normal domain admin, so they
cannot be set while `domainGlobalAdmin=yes`.
`maxDomains`: how many mail domains this admin can create.
`maxQuota`: how much mailbox quota this admin can create.
Quota is shared by all domains created/managed by this
admin. Sample: 10, 20, 30. Must be used with @quotaUnit.
`quotaUnit`: quota unit of @maxQuota. Must be used with @maxQuota.
`maxUsers`: how many mail users this admin can create.
It's shared by all domains created/managed by this admin.
`maxAliases`: how many mail aliases this admin can create.
It's shared by all domains created/managed by this admin.
`maxUsers`: how many mailing lists this admin can create.
It's shared by all domains created/managed by this admin.
"""
form = web.input()
form['mail'] = mail
form['cn'] = form.get('name')
form['newpw'] = form.get('password')
form['confirmpw'] = form.get('password')
form['domainGlobalAdmin'] = form.get('isGlobalAdmin')
form['preferredLanguage'] = form.get('language')
for (k_api, k_web) in _param_maps:
if k_api in form:
form[k_web] = form[k_api]
# [(api_form_name, web_form_name), ...]
for (k_api, k_web) in [('disableViewingMailLog', 'disable_viewing_mail_log'),
('disableManagingQuarantinedMails', 'disable_managing_quarantined_mails')]:
v = form.get(k_api, '')
if v == 'yes':
form[k_web] = 'yes'
qr = sql_lib_admin.add_admin_from_form(form=form)
return api_render(qr)
@decorators.api_require_global_admin
def DELETE(self, mail):
"""Delete an existing mail domain.
curl -X DELETE -i -b cookie.txt https://<server>/api/admin/<mail>
"""
qr = sql_lib_admin.delete_admins(mails=[mail], revoke_admin_privilege_from_user=False)
return api_render(qr)
@decorators.api_require_global_admin
def PUT(self, mail):
"""Update profile of existing standalone domain admin.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/domain/<domain>
curl -X PUT -i -b cookie.txt -d "var=<value>&var2=<value2>" https://<server>/api/domain/<domain>
:param mail: full admin email address.
Form parameters:
`name`: the display name of this admin
`password`: admin's password
`accountStatus`: account status (active, disabled)
`domainGlobalAdmin`: Mark this admin as global admin (yes, no).
`language`: default preferred language for new user.
e.g. en_US for English, de_DE for Deutsch.
Form parameters listed below are used by normal domain admin, so they
cannot be set while `domainGlobalAdmin=yes`.
`maxDomains`: how many mail domains this admin can create.
`maxQuota`: how much mailbox quota this admin can create.
Quota is shared by all domains created/managed by this
admin. Sample: 10, 20, 30. Must be used with @quotaUnit.
`quotaUnit`: quota unit of @maxQuota. Must be used with @maxQuota.
`maxUsers`: how many mail users this admin can create.
It's shared by all domains created/managed by this admin.
`maxAliases`: how many mail aliases this admin can create.
It's shared by all domains created/managed by this admin.
`maxUsers`: how many mailing lists this admin can create.
It's shared by all domains created/managed by this admin.
"""
form = web.input()
for (k_api, k_web) in _param_maps:
if k_api in form:
form[k_web] = form[k_api]
qr = sql_lib_admin.api_update_profile(form=form, mail=mail)
return api_render(qr)

View File

@@ -0,0 +1,243 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from controllers.utils import api_render
from libs import iredutils, form_utils
from libs.logger import log_activity
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import general as sql_lib_general
from libs.sqllib import alias as sql_lib_alias
from libs.sqllib import api_utils
session = web.config.get('_session')
class APIAlias:
@decorators.api_require_domain_access
def GET(self, mail):
"""Export mail alias profile.
curl -X GET -i -b cookie.txt https://<server>/api/alias/<mail>
"""
mail = str(mail).lower()
qr = sql_lib_alias.get_profile(mail=mail, conn=None)
if qr[0]:
profile = api_utils.export_sql_record(record=qr[1])
return api_render((True, profile))
else:
return api_render(qr)
@decorators.api_require_domain_access
def POST(self, mail):
"""Create a new mail alias account.
curl -X POST -i -b cookie.txt -d "..." https://<server>/api/alias/<email>
Optional POST data:
@name - display name
@accessPolicy - access policy
@members - members of mail alias
"""
mail = str(mail).lower()
(listname, domain) = mail.split('@', 1)
form = web.input()
form['listname'] = listname
form['domainName'] = domain
form['cn'] = form.get('name')
qr = sql_lib_alias.add_alias_from_form(domain=domain, form=form)
if qr[0] and 'members' in form:
# Update mail forwarding addresses
_addresses = form_utils.get_multi_values_from_api(form=form,
input_name='members',
to_lowercase=False,
is_email=True)
_qr = sql_lib_alias.reset_members(mail=mail, members=_addresses)
return api_render(_qr)
return api_render(qr)
# Delete aliases.
@decorators.api_require_domain_access
def DELETE(self, mail):
"""Delete a mail alias account.
curl -X DELETE -i -b cookie.txt https://<server>/api/alias/<mail>
"""
mail = str(mail).lower()
qr = sql_lib_alias.delete_aliases(accounts=[mail])
return api_render(qr)
@decorators.api_require_domain_access
def PUT(self, mail):
"""Update profile of existing mail alias account.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/alias/<mail>
curl -X PUT -i -b cookie.txt -d "var=<value>&var2=<value2>" https://<server>/api/alias/<mail>
Optional PUT data:
@name - common name (or, display name)
@accountStatus - enable or disable user. possible value is: active, disabled.
@accessPolicy - access policy.
@members - members of mail alias
@addMember - add new members to mailing list
@removeMember - remove members from mailing list
"""
mail = str(mail).lower()
domain = mail.split('@', 1)[-1]
form = web.input()
params = {}
# Name
kv = form_utils.get_form_dict(form,
input_name='name',
key_name='name',
default_value='')
params.update(kv)
# accountStatus
kv = form_utils.get_form_dict(form,
input_name='accountStatus',
key_name='active',
default_value='1')
params.update(kv)
# Access policy
kv = form_utils.get_form_dict(form,
input_name='accessPolicy',
key_name='accesspolicy',
default_value='public')
params.update(kv)
# Reset all members
_members = []
# Add new members
_new = []
# Remove existing members
_removed = []
if 'members' in form:
# Update mail forwarding addresses
_v = form_utils.get_multi_values_from_api(form=form,
input_name='members',
to_lowercase=False,
is_email=True)
_members = [iredutils.lower_email_with_upper_ext_address(i) for i in _v]
else:
if 'addMember' in form:
_v = form_utils.get_multi_values_from_api(form=form,
input_name='addMember',
to_lowercase=False,
is_email=True)
_new = [iredutils.lower_email_with_upper_ext_address(i) for i in _v]
if 'removeMember' in form:
_v = form_utils.get_multi_values_from_api(form=form,
input_name='removeMember',
to_lowercase=False,
is_email=True)
_removed = [iredutils.lower_email_with_upper_ext_address(i) for i in _v]
if not (params or ('members' in form) or _new or _removed):
return api_render(True)
_wrap = SQLWrap()
conn = _wrap.conn
if not sql_lib_general.is_alias_exists(mail=mail, conn=conn):
return api_render((False, 'NO_SUCH_ACCOUNT'))
if params:
try:
conn.update('alias',
vars={'mail': mail},
where='address=$mail',
**params)
log_activity(msg="Update alias profile: {} -> {}".format(mail, ', '.join(params)),
admin=session.get('username'),
username=mail,
domain=domain,
event='update')
except Exception as e:
return api_render((False, repr(e)))
if 'members' in form:
qr = sql_lib_alias.reset_members(mail=mail, members=_members, conn=conn)
if not qr[0]:
return api_render(qr)
if _new or _removed:
qr = sql_lib_alias.update_members(mail=mail,
new_members=_new,
removed_members=_removed,
conn=conn)
if not qr[0]:
return api_render(qr)
return api_render(True)
class APIChangeEmail:
@decorators.api_require_domain_access
def POST(self, mail, new_mail):
"""Change email address of mail alias account.
curl -X POST -i -b cookie.txt https://<server>/api/alias/<mail>/change_email/<new_mail>
"""
mail = str(mail).lower()
new_mail = str(new_mail).lower()
qr = sql_lib_alias.change_email(mail=mail, new_mail=new_mail)
return api_render(qr)
class APIAliases:
@decorators.api_require_domain_access
def GET(self, domain):
"""List all mail aliases under given domain.
curl -X GET -i -b cookie.txt https://<server>/api/aliases/<domain>
Optional parameters:
@email_only -- return a list of email addresses.
if not present, return a list of account profiles
(dicts).
@disabled_only -- return disabled accounts.
"""
domain = str(domain).lower()
form = web.input(_unicode=True)
email_only = ('email_only' in form)
disabled_only = ('disabled_only' in form)
qr = sql_lib_alias.get_basic_alias_profiles(domain=domain,
email_only=email_only,
disabled_only=disabled_only,
conn=None)
if qr[0]:
if email_only:
emails = qr[1]
return api_render((True, emails))
else:
profiles = api_utils.export_sql_records(records=qr[1])
return api_render((True, profiles))
else:
return api_render(qr)

View File

@@ -0,0 +1,237 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from controllers.utils import api_render
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import general as sql_lib_general
from libs.sqllib import api_utils
session = web.config.get('_session')
class APIDomains:
@decorators.api_require_admin_login
def GET(self):
"""Get all managed domains.
curl -X GET -i -b cookie.txt https://<server>/api/domains
curl -X GET -i -b cookie.txt https://<server>/api/domains?name_only=
curl -X GET -i -b cookie.txt https://<server>/api/domains?name_only=&disabled_only=
Optional parameters:
@name_only - Return only domain names, no profiles.
@disabled_only - Return profiles of disabled domains.
Values of above 2 parameters don't matter at all, for example, these 2
values are the same: `name_only=`, `name_only=yes`.
"""
name_only = False
disabled_only = False
form = web.input()
if 'name_only' in form:
name_only = True
if 'disabled_only' in form:
disabled_only = True
qr = sql_lib_domain.get_all_managed_domains(name_only=name_only, disabled_only=disabled_only)
if qr[0]:
if name_only:
return api_render((True, qr[1]))
else:
profiles = {}
for i in qr[1]:
domain = str(i.domain).lower()
profiles[domain] = api_utils.export_sql_record(record=i)
return api_render((True, profiles))
else:
return api_render(qr)
class APIDomain:
@decorators.api_require_domain_access
def GET(self, domain):
"""Export SQL record of mail domain as a dict.
curl -X GET -i -b cookie.txt https://<server>/api/domain/<domain>
"""
domain = str(domain).lower()
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_domain.profile(domain=domain)
if qr[0]:
profile = api_utils.export_sql_record(record=qr[1])
#
# Get all alias domains
#
_qr = sql_lib_domain.get_all_alias_domains(domain=domain,
name_only=True,
conn=conn)
if _qr[0]:
profile['alias_domains'] = _qr[1]
#
# Get per-domain sender dependent relayhost
#
(_status, _result) = sql_lib_general.get_sender_relayhost(sender='@' + domain)
if _status:
profile['relayhost'] = _result
#
# Get allocated domain quota
#
_quota = sql_lib_domain.get_allocated_domain_quota(domains=[domain])
profile['allocated_quota'] = _quota
return api_render((True, profile))
else:
return api_render(qr)
@decorators.api_require_global_admin
def POST(self, domain):
"""Create a new domain.
curl -X POST -i -b cookie.txt -d "defaultQuota=1024" https://<server>/api/domain/<new_domain>
Parameters:
@name - the short description of this domain name. e.g. company name.
@quota - per-domain mailbox quota, in MB.
@language - default preferred language for new user.
e.g. en_US for English, de_DE for Deutsch.
@transport - per-domain transport
@defaultQuota - default mailbox quota for new user.
@maxUserQuota - Max mailbox quota of a single mail user
@numberOfUsers - Max number of mail user accounts
@numberOfAliases - Max number of mail alias accounts
"""
form = web.input()
form['domainName'] = domain
form['cn'] = form.get('name')
form['preferredLanguage'] = form.get('language', '')
form['mtaTransport'] = form.get('transport', '')
form['domainQuota'] = form.get('quota')
form['domainQuotaUnit'] = 'MB'
qr = sql_lib_domain.add(form=form)
return api_render(qr)
@decorators.api_require_domain_access
def DELETE(self, domain, keep_mailbox_days=0):
"""Delete an existing mail domain.
curl -X DELETE -i -b cookie.txt https://<server>/api/domain/<domain>
curl -X DELETE -i -b cookie.txt https://<server>/api/domain/<domain>/keep_mailbox_days/<days>
"""
qr = sql_lib_domain.delete_domains(domains=[domain], keep_mailbox_days=keep_mailbox_days)
return api_render(qr)
@decorators.api_require_domain_access
def PUT(self, domain):
"""Update domain profile.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/domain/<domain>
curl -X PUT -i -b cookie.txt -d "var=<value>&var2=<value2>" https://<server>/api/domain/<domain>
:param domain: domain name.
Form parameters:
`name`: the short company/orgnization name
`accountStatus`: enable or disable domain. possible value is: active, disabled.
`quota`: Per-domain mailbox quota
`transport`: Per-domain transport
`language`: default preferred language for new user.
e.g. en_US for English, de_DE for Deutsch.
`minPasswordLength`: Minimal password length
`maxPasswordLength`: Maximum password length
`defaultQuota`: default mailbox quota for new user.
`maxUserQuota`: Max mailbox quota of a single mail user
`numberOfUsers`: Max number of mail user accounts
`numberOfAliases`: Max number of mail alias accounts
`senderBcc`: set bcc address for outgoing emails
`recipientBcc`: set bcc address for incoming emails
`catchall`: set per-domain catch-all account.
catchall account is a list of email address which will
receive emails sent to non-existing address under same
domain
`outboundRelay`: relay outgoing emails to specified host
`addService`: enable new services. Multiple services must be separated by comma.
`removeService`: disable existing services. Multiple services must be separated by comma.
`services`: reset all services. If empty, all existing services will be removed.
`disableDomainProfile`: disable given domain profiles. Normal admin
cannot view and update disabled profiles in
domain profile page.
`enableDomainProfile`: enable given domain profiles. Normal admin
can view and update disabled profiles in
domain profile page.
`disableUserProfile`: disable given user profiles. Normal admin
cannot view and update disabled profiles in
user profile page.
`enableUserProfile`: enable given domain profiles. Normal admin
can view and update disabled profiles in
user profile page.
`disableUserPreference`: disable given user preferences in
self-service page. Normal mail user cannot
view and update disabled preferences.
`enableUserPreference`: disable given user preferences in
self-service page. Normal mail user can
view and update disabled preferences.
`aliasDomains`: remove all existing alias domains and add given
domains as alias domains. Multiple domains must be
separated by comma.
`addAliasDomain`: add new alias domains. Multiple domains must be
separated by comma.
`removeAliasDomain`: remove existing alias domains. Multiple
domains must be separated by comma.
"""
form = web.input()
qr = sql_lib_domain.api_update_profile(domain=domain, form=form)
return api_render(qr)
class APIDomainAdmin:
@decorators.api_require_domain_access
def GET(self, domain):
"""List all existing domain admins.
curl -X GET -i -b cookie.txt https://<server>/api/domain/admins/<domain>
"""
qr = sql_lib_domain.get_domain_admin_addresses(domain=domain)
return api_render(qr)
@decorators.api_require_domain_access
def PUT(self, domain):
"""Update domain admins.
curl -X PUT -i -b cookie.txt -d "var=<value>[,<value2>,...]" https://<server>/api/domain/admins/<domain>
Parameters:
@addAdmin - Add new domain admin. Multiple admins must be separated by comma.
@removeAdmin - Remove existing domain admin. Multiple admins must be separated by comma.
@removeAllAdmin - Remove all existing domain admins.
"""
form = web.input()
qr = sql_lib_domain.api_update_domain_admins(domain=domain, form=form)
return api_render(qr)

View File

@@ -0,0 +1,45 @@
import web
from controllers.utils import api_render
from libs import iredpwd
from libs.sqllib import decorators
from libs.sqllib import user as sql_lib_user
from libs.sqllib import admin as sql_lib_admin
class APIVerifyPassword:
@decorators.api_require_global_admin
def POST(self, account_type, mail):
"""Verify submitted (plain) password against the one stored in SQL db.
curl -X POST -i -b cookie.txt -d "var=<value>" https://<server>/api/verify_password/user/<mail>
curl -X POST -i -b cookie.txt -d "var=<value>" https://<server>/api/verify_password/admin/<mail>
Required parameters:
@password - plain password you want to verify
"""
mail = str(mail).lower()
form = web.input()
pw = form.get('password', '')
if not pw:
return api_render((False, 'EMPTY_PASSSWORD'))
try:
if account_type == 'admin':
qr = sql_lib_admin.get_profile(mail=mail, columns=['password'], conn=None)
else:
qr = sql_lib_user.simple_profile(mail=mail, columns=['password'])
if qr[0]:
pw_in_db = str(qr[1].password)
qr_pw = iredpwd.verify_password_hash(pw_in_db, pw)
return api_render(qr_pw)
else:
return api_render(qr)
except Exception as e:
return api_render((False, repr(e)))

123
controllers/sql/api_ml.py Normal file
View File

@@ -0,0 +1,123 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from controllers.utils import api_render
from libs.sqllib import decorators
from libs.sqllib import ml as sql_lib_ml
from libs.sqllib import api_utils
session = web.config.get('_session')
class APIMLS:
@decorators.api_require_domain_access
def GET(self, domain):
"""List all mailing lists in given domain.
curl -X GET -i -b cookie.txt https://<server>/api/mls/<domain>
Optional parameters:
@email_only -- return a list of mailing list addresses.
if not present, return a list of mailing list profiles
(dicts).
"""
domain = str(domain).lower()
form = web.input(_unicode=True)
email_only = ('email_only' in form)
qr = sql_lib_ml.get_basic_ml_profiles(domain=domain,
email_only=email_only,
conn=None)
if qr[0]:
if email_only:
emails = qr[1]
return api_render((True, emails))
else:
profiles = api_utils.export_sql_records(records=qr[1])
return api_render((True, profiles))
else:
return api_render(qr)
class APIML:
@decorators.api_require_domain_access
def GET(self, mail):
"""Export mailing list profile.
curl -X GET -i -b cookie.txt https://<server>/api/ml/<mail>
Optional arguments:
@with_subscribers -- if set to 'yes', all subscribers will be returned.
"""
mail = str(mail).lower()
form = web.input(_unicode=False)
with_subscribers = ('with_subscribers' in form)
qr = sql_lib_ml.get_profile(mail=mail,
with_subscribers=with_subscribers,
conn=None)
if qr[0]:
profile = api_utils.export_sql_record(record=qr[1])
return api_render((True, profile))
else:
return api_render(qr)
@decorators.api_require_domain_access
def DELETE(self, mail):
"""Delete a mailing list.
curl -X DELETE -i -b cookie.txt https://<server>/api/ml/<mail>
"""
mail = str(mail).lower()
form = web.input()
keep_archive = True
if form.get('keep_archive') == 'no':
keep_archive = False
qr = sql_lib_ml.delete_maillists(accounts=[mail],
keep_archive=keep_archive,
conn=None)
return api_render(qr)
@decorators.api_require_domain_access
def POST(self, mail):
"""Create a new mail alias account.
curl -X POST -i -b cookie.txt -d "..." https://<server>/api/ml/<email>
Optional POST parameters:
"""
mail = str(mail).lower()
(listname, domain) = mail.split('@', 1)
form = web.input()
form['listname'] = listname
form['domainName'] = domain
qr = sql_lib_ml.add_ml_from_web_form(domain=domain, form=form, conn=None)
return api_render(qr)
@decorators.api_require_domain_access
def PUT(self, mail):
"""Update mailing list profile.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/ml/<mail>
curl -X PUT -i -b cookie.txt -d "var=<value>&var2=<value2>" https://<server>/ml/<mail>
Optional PUT data:
"""
mail = str(mail).lower()
form = web.input()
qr = sql_lib_ml.api_update_profile(mail=mail, form=form, conn=None)
return api_render(qr)

340
controllers/sql/api_user.py Normal file
View File

@@ -0,0 +1,340 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from controllers.utils import api_render
from libs import form_utils, iredpwd
from libs.logger import log_activity
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import user as sql_lib_user
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import general as sql_lib_general
from libs.sqllib import api_utils
session = web.config.get('_session')
class APIUser:
@decorators.api_require_domain_access
def GET(self, mail):
"""Export SQL record of mail user as a dict.
curl -X GET -i -b cookie.txt https://<server>/api/user/<mail>
"""
mail = str(mail).lower()
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_user.profile(mail=mail,
with_aliases=True,
with_alias_groups=True,
with_mailing_lists=True,
with_forwardings=True,
with_used_quota=True,
with_last_login=True,
conn=conn)
if qr[0]:
profile = api_utils.export_sql_record(record=qr[1],
remove_columns=settings.API_HIDDEN_USER_PROFILES)
if profile.get('isadmin') == 1:
# Get all managed domains
_qr = sql_lib_admin.get_managed_domains(admin=mail,
domain_name_only=True,
listed_only=True,
conn=conn)
if _qr[0]:
profile['managed_domains'] = _qr[1]
return api_render((True, profile))
else:
return api_render(qr)
@decorators.api_require_domain_access
def POST(self, mail):
"""Create a new mail user.
curl -X POST -i -b cookie.txt -d "var=value1&var2=value2&..." https://<server>/api/user/<mail>
Optional POST data:
@name - display name
@password - password
@password_hash - password hash
@language - default preferred language for new user. e.g.
en_US for English, de_DE for Deutsch.
@quota - mailbox quota for this user (in MB).
"""
mail = str(mail).lower()
(username, domain) = mail.split('@', 1)
if not session.get('is_global_admin'):
sql_lib_user.redirect_if_user_is_global_admin(conn=None, mail=mail)
form = web.input()
form['username'] = username
form['domainName'] = domain
form['preferredLanguage'] = form.get('language')
form['cn'] = form.get('name')
_pw = form.get('password', '')
if _pw:
form['newpw'] = _pw
form['confirmpw'] = _pw
else:
_pw_hash = form.get('password_hash', '')
form['password_hash'] = _pw_hash
# Set quota
form['mailQuota'] = form.get('quota')
qr = sql_lib_user.add_user_from_form(domain=domain, form=form)
return api_render(qr)
@decorators.api_require_domain_access
def DELETE(self, mail, keep_mailbox_days=0):
"""Delete a mail user.
curl -X DELETE -i -b cookie.txt https://<server>/api/user/<mail>
curl -X DELETE -i -b cookie.txt https://<server>/api/user/<mail>/keep_mailbox_days/<days>
"""
mail = str(mail).lower()
_wrap = SQLWrap()
conn = _wrap.conn
if not session.get('is_global_admin'):
sql_lib_user.redirect_if_user_is_global_admin(conn=conn, mail=mail)
qr = sql_lib_user.delete_users(conn=conn, accounts=[mail], keep_mailbox_days=keep_mailbox_days)
return api_render(qr)
@decorators.api_require_domain_access
def PUT(self, mail):
"""Update user profile.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/user/<mail>
curl -X PUT -i -b cookie.txt -d "var=<value>&var2=<value2>" https://<server>/api/user/<mail>
Optional PUT data:
@name - common name (or, display name)
@password - set new password for user
@password_hash - set new password to given hashed password
@quota - mailbox quota for this user (in MB).
@accountStatus - enable or disable user. possible value is: active, disabled.
@language - set preferred language of web UI
@employeeid - set employee id
@transport - set per-user transport
@isGlobalAdmin -- promote user to be a global admin
@forwarding -- set per-user mail forwarding addresseses
@addForwarding -- add per-user mail forwarding addresses
@removeForwarding -- remove existing per-user mail forwarding addresses
@senderBcc -- set per-user bcc for outbound emails
@recipientBcc -- set per-user bcc for inbound emails
@aliases -- reset per-user alias addresses
@addAlias -- add new per-user alias addresses
@removeAlias -- remove existing per-user alias addresses
@maildir -- full maildir path of the mailbox
"""
mail = str(mail).lower()
form = web.input()
qr = sql_lib_user.api_update_profile(mail=mail, form=form, conn=None)
return api_render(qr)
class APIUsers:
@decorators.api_require_domain_access
def GET(self, domain):
"""List all mail users in given domain.
curl -X GET -i -b cookie.txt https://<server>/api/users/<domain>
Optional parameters:
@email_only -- return a list of users' email addresses.
if not present, return a list of user profiles
(dicts).
@disabled_only -- return disabled users.
"""
domain = str(domain).lower()
form = web.input(_unicode=True)
email_only = ('email_only' in form)
disabled_only = ('disabled_only' in form)
qr = sql_lib_user.get_basic_user_profiles(domain=domain,
email_only=email_only,
disabled_only=disabled_only,
with_last_login=True,
with_used_quota=True,
conn=None)
if qr[0]:
if email_only:
emails = qr[1]
return api_render((True, emails))
else:
profiles = api_utils.export_sql_records(records=qr[1])
return api_render((True, profiles))
else:
return api_render(qr)
@decorators.api_require_domain_access
def PUT(self, domain):
"""Update profiles of users under domain.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/users/<domain>
curl -X PUT -i -b cookie.txt -d "var=<value>&var2=<value2>" https://<server>/api/users/<domain>
Optional PUT data:
@name - common name (or, display name)
@accountStatus - enable or disable user. possible value is: active, disabled.
@language - set preferred language of web UI
@transport - set per-user transport
@password - reset all users' password.
"""
domain = str(domain).lower()
form = web.input()
params = {}
# Name
kv = form_utils.get_form_dict(form,
input_name='name',
key_name='name')
params.update(kv)
# Account status
kv = form_utils.get_form_dict(form,
input_name='accountStatus',
key_name='active')
params.update(kv)
# Language
kv = form_utils.get_form_dict(form,
input_name='language',
to_string=True)
params.update(kv)
# Transport
kv = form_utils.get_form_dict(form,
input_name='transport',
to_string=True)
params.update(kv)
_wrap = SQLWrap()
conn = _wrap.conn
# Password
if "password" in form:
pw = form_utils.get_single_value(form,
input_name="password",
default_value="",
to_string=True)
if not pw:
return api_render((False, "EMPTY_PASSWORD"))
qr = sql_lib_general.get_domain_settings(domain=domain, conn=conn)
if not qr[0]:
return api_render(qr)
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 = iredpwd.verify_new_password(newpw=pw,
confirmpw=pw,
min_passwd_length=min_passwd_length,
max_passwd_length=max_passwd_length)
if qr[0]:
params["password"] = iredpwd.generate_password_hash(pw)
else:
return api_render(qr)
if not params:
return api_render(True)
try:
conn.update('mailbox',
vars={'domain': domain},
where='domain=$domain',
**params)
try:
# Log updated parameters and values if possible
msg = str(params)
except:
msg = ', '.join(params)
log_activity(msg="Update profiles of all users under domain: {} -> {}".format(domain, msg),
admin=session.get('username'),
username=domain,
domain=domain,
event='update')
return api_render(True)
except Exception as e:
return api_render((False, repr(e)))
class APIUsersPassword:
@decorators.api_require_domain_access
def PUT(self, domain):
"""Update password of all users under domain.
curl -X PUT -i -b cookie.txt -d "var=<value>" https://<server>/api/users/<domain>/password
Required parameters:
@password - set new password for user
"""
domain = str(domain).lower()
form = web.input()
qr = api_utils.get_form_password_dict(form=form,
domain=domain,
input_name='password')
if qr[0]:
pw_hash = qr[1]['pw_hash']
_wrap = SQLWrap()
conn = _wrap.conn
conn.update('mailbox',
vars={'domain': domain},
where='domain=$domain',
password=pw_hash)
log_activity(msg="Update all users' password under domain: %s" % domain,
admin=session.get('username'),
username=domain,
domain=domain,
event='update')
return api_render(True)
else:
return api_render(qr)
class APIChangeEmail:
@decorators.api_require_domain_access
def POST(self, mail, new_mail):
"""Change user email address.
curl -X POST -i -b cookie.txt https://<server>/api/user/<mail>/change_email/<new_mail>
"""
mail = str(mail).lower()
new_mail = str(new_mail).lower()
qr = sql_lib_user.change_email(mail=mail, new_mail=new_mail)
return api_render(qr)

501
controllers/sql/basic.py Normal file
View File

@@ -0,0 +1,501 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from controllers.utils import api_render
from libs import __version_sql__ as __version__
from libs import iredutils, sysinfo, form_utils
from libs.logger import logger, log_activity
from libs.sqllib import SQLWrap, auth, decorators
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import utils as sql_lib_utils
from libs.sqllib import general as sql_lib_general
if settings.iredapd_enabled:
from libs.iredapd import log as iredapd_log
if settings.fail2ban_enabled:
from libs.f2b import log as f2b_log
if settings.amavisd_enable_quarantine or settings.amavisd_enable_logging:
from libs.amavisd import log as lib_amavisd_log
session = web.config.get('_session')
class Login:
def GET(self):
if not session.get('logged'):
form = web.input(_unicode=False)
if not iredutils.is_allowed_admin_login_ip(client_ip=web.ctx.ip):
return web.render('error_without_login.html',
error='NOT_ALLOWED_IP')
# Show login page.
return web.render('login.html',
languagemaps=iredutils.get_language_maps(),
msg=form.get('msg'))
else:
if session.get('account_is_mail_user'):
iredutils.self_service_login_redirect(session['username'])
else:
if settings.REDIRECT_TO_DOMAIN_LIST_AFTER_LOGIN:
raise web.seeother('/domains')
else:
raise web.seeother('/dashboard')
def POST(self):
# Get username, password.
form = web.input(_unicode=False)
username = form.get('username', '').strip().lower()
password = str(form.get('password', '').strip())
domain = username.split('@', 1)[-1]
# Auth as domain admin
_wrap = SQLWrap()
conn = _wrap.conn
auth_result = auth.auth(conn=conn,
username=username,
password=password,
account_type='admin')
if auth_result[0]:
log_activity(msg="Admin login success.", domain=domain, event='login')
# Save selected language
selected_language = str(form.get('lang', '')).strip()
if selected_language != web.ctx.lang and \
selected_language in iredutils.get_language_maps():
session['lang'] = selected_language
account_settings = auth_result[1].get('account_settings', {})
if (not session.get('is_global_admin')) and 'create_new_domains' in account_settings:
session['create_new_domains'] = True
for k in ['disable_viewing_mail_log',
'disable_managing_quarantined_mails']:
if account_settings.get(k) == 'yes':
session[k] = True
if settings.REDIRECT_TO_DOMAIN_LIST_AFTER_LOGIN:
raise web.seeother('/domains')
else:
raise web.seeother('/dashboard?checknew')
else:
#
# User login for self-service
#
# Check enabled services.
qr = sql_lib_domain.get_domain_enabled_services(domain=domain, conn=conn)
if qr[0]:
enabled_services = qr[1]
if 'self-service' not in enabled_services:
# domain doesn't allow self-service
raise web.seeother('/login?msg=INVALID_CREDENTIALS')
else:
raise web.seeother('/login?msg=INVALID_CREDENTIALS')
user_auth_result = auth.auth(conn=conn,
username=username,
password=password,
account_type='user')
if user_auth_result[0]:
log_activity(msg="User login success", event='user_login')
account_settings = user_auth_result[1].get('account_settings', {})
if (not session.get('is_global_admin')) and \
'create_new_domains' in account_settings:
session['create_new_domains'] = True
iredutils.self_service_login_redirect(session['username'])
else:
session['failed_times'] += 1
logger.warning("Web login failed: client_address={}, username={}".format(web.ctx.ip, username))
log_activity(msg="Login failed.", admin=username, event='login', loglevel='error')
raise web.seeother('/login?msg=%s' % web.urlquote(auth_result[1]))
class Logout:
def GET(self):
try:
session.kill()
except:
pass
raise web.seeother('/login')
class Dashboard:
@decorators.require_admin_login
def GET(self):
form = web.input(_unicode=False)
_check_new_version = ('checknew' in form)
# Check new version.
if session.get('is_global_admin') and _check_new_version:
(_status, _info) = sysinfo.check_new_version()
session['new_version_available'] = _status
if _status:
session['new_version'] = _info
else:
session['new_version_check_error'] = _info
# Get numbers of domains, users, aliases.
num_existing_domains = 0
num_existing_users = 0
num_existing_lists = 0
num_existing_aliases = 0
_wrap = SQLWrap()
conn = _wrap.conn
try:
num_existing_domains = sql_lib_admin.num_managed_domains(conn=conn)
num_existing_users = sql_lib_admin.num_managed_users(conn=conn)
num_existing_lists = sql_lib_admin.num_managed_lists(conn=conn)
num_existing_aliases = sql_lib_admin.num_managed_aliases(conn=conn)
except:
pass
#
# For normal domain admin
#
# Get number of max domains/users,aliases. (-1 means no limitation)
num_max_domains = -1
num_max_users = -1
num_max_lists = -1
num_max_aliases = -1
admin = session.get('username')
if (not session.get('is_global_admin')) and session.get('create_new_domains'):
# Get account settings
qr = sql_lib_general.get_admin_settings(admin=admin, conn=conn)
if qr[0]:
account_settings = qr[1]
num_max_domains = account_settings.get('create_max_domains', -1)
num_max_users = account_settings.get('create_max_users', -1)
num_max_lists = account_settings.get('create_max_lists', -1)
num_max_aliases = account_settings.get('create_max_aliases', -1)
# Get numbers of existing messages and quota bytes.
# Set None as default, so that it's easy to detect them in Jinja2 template.
total_messages = None
total_bytes = None
if session.get('is_global_admin'):
if settings.SHOW_USED_QUOTA:
try:
qr = sql_lib_admin.sum_all_used_quota(conn=conn)
total_messages = qr['messages']
total_bytes = qr['bytes']
except:
pass
# Get number of incoming/outgoing emails in latest 24 hours.
last_hours = settings.STATISTICS_HOURS
last_seconds = last_hours * 60 * 60
num_incoming_mails = 0
num_outgoing_mails = 0
num_virus = 0
num_quarantined = 0
# iRedAPD
num_rejected = 0
num_smtp_outbound_sessions = 0
top_senders = []
top_recipients = []
all_reversed_domain_names = []
if settings.amavisd_enable_logging or settings.amavisd_enable_quarantine:
# Get all managed domain names and reversed names.
_all_domains = []
result_all_domains = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.get('username'),
domain_name_only=True)
if result_all_domains[0]:
_all_domains += result_all_domains[1]
all_reversed_domain_names = iredutils.reverse_amavisd_domain_names(_all_domains)
if settings.amavisd_enable_logging:
num_incoming_mails = lib_amavisd_log.count_incoming_mails(all_reversed_domain_names, last_seconds)
num_outgoing_mails = lib_amavisd_log.count_outgoing_mails(all_reversed_domain_names, last_seconds)
num_virus = lib_amavisd_log.count_virus_mails(all_reversed_domain_names, last_seconds)
top_senders = lib_amavisd_log.get_top_users(
reversedDomainNames=all_reversed_domain_names,
log_type='sent',
timeLength=last_seconds,
number=settings.NUM_TOP_SENDERS,
)
top_recipients = lib_amavisd_log.get_top_users(
reversedDomainNames=all_reversed_domain_names,
log_type='received',
timeLength=last_seconds,
number=settings.NUM_TOP_RECIPIENTS,
)
# Get records of quarantined mails.
if settings.amavisd_enable_quarantine:
num_quarantined = lib_amavisd_log.count_quarantined(all_reversed_domain_names, last_seconds)
if settings.iredapd_enabled:
num_rejected = iredapd_log.get_num_rejected(hours=last_hours)
num_smtp_outbound_sessions = iredapd_log.get_num_smtp_outbound_sessions(
hours=last_hours,
)
num_banned = 0
if session.get('is_global_admin') and settings.fail2ban_enabled:
num_banned = f2b_log.num_banned()
return web.render(
'dashboard.html',
version=__version__,
iredmail_version=sysinfo.get_iredmail_version(),
hostname=sysinfo.get_hostname(),
uptime=sysinfo.get_server_uptime(),
loadavg=sysinfo.get_system_load_average(),
netif_data=sysinfo.get_nic_info(),
# number of existing accounts
num_existing_domains=num_existing_domains,
num_existing_users=num_existing_users,
num_existing_lists=num_existing_lists,
num_existing_aliases=num_existing_aliases,
# number of account limitation
num_max_domains=num_max_domains,
num_max_users=num_max_users,
num_max_lists=num_max_lists,
num_max_aliases=num_max_aliases,
total_messages=total_messages,
total_bytes=total_bytes,
# amavisd statistics
num_incoming_mails=num_incoming_mails,
num_outgoing_mails=num_outgoing_mails,
num_virus=num_virus,
num_quarantined=num_quarantined,
top_senders=top_senders,
top_recipients=top_recipients,
removeQuarantinedInDays=settings.AMAVISD_REMOVE_QUARANTINED_IN_DAYS,
# iRedAPD
num_rejected=num_rejected,
num_smtp_outbound_sessions=num_smtp_outbound_sessions,
# Fail2ban
num_banned=num_banned,
)
class Search:
@decorators.require_admin_login
def GET(self):
form = web.input()
return web.render('sql/search.html', msg=form.get('msg'))
@decorators.csrf_protected
@decorators.require_admin_login
def POST(self):
form = web.input(account_type=[], accountStatus=[])
search_string = form.get('searchString', '').strip()
if not search_string:
raise web.seeother('/search?msg=EMPTY_STRING')
account_type = form.get('account_type', [])
account_status = form.get('accountStatus', [])
try:
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_utils.search(conn=conn,
search_string=search_string,
account_type=account_type,
account_status=account_status)
if not qr[0]:
return web.render('sql/search.html',
msg=qr[1],
searchString=search_string)
except Exception as e:
return web.render('sql/search.html',
msg=repr(e),
searchString=search_string)
# Group account types.
domains = qr[1].get('domain', [])
admins = qr[1].get('admin', [])
users = qr[1].get('user', [])
mls = qr[1].get('ml', [])
last_logins = qr[1]['last_logins']
user_alias_addresses = qr[1]['user_alias_addresses']
user_forwarding_addresses = qr[1]['user_forwarding_addresses']
user_assigned_groups = qr[1]['user_assigned_groups']
aliases = qr[1].get('alias', [])
all_global_admins = qr[1].get('allGlobalAdmins', [])
total_results = len(domains) + len(admins) + len(users) + len(aliases) + len(mls)
if session.get('is_global_admin'):
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX_FOR_GLOBAL_ADMIN
else:
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX
return web.render('sql/search.html',
searchString=search_string,
total_results=total_results,
domains=domains,
admins=admins,
users=users,
mls=mls,
last_logins=last_logins,
user_alias_addresses=user_alias_addresses,
user_forwarding_addresses=user_forwarding_addresses,
user_assigned_groups=user_assigned_groups,
aliases=aliases,
allGlobalAdmins=all_global_admins,
days_to_keep_removed_mailbox=days_to_keep_removed_mailbox,
msg=form.get('msg'))
class OperationsFromSearchPage:
@decorators.require_admin_login
def GET(self, *args, **kw):
raise web.seeother('/search')
@decorators.csrf_protected
@decorators.require_admin_login
def POST(self, account_type):
account_type = web.safestr(account_type)
form = web.input(_unicode=False, mail=[])
# Get action.
action = form.get('action', None)
if action not in ['enable', 'disable', 'delete']:
raise web.seeother('/search?msg=INVALID_ACTION')
# Get list of accounts which has valid format.
accounts = [web.safestr(v).lower()
for v in form.get('mail', [])
if iredutils.is_email(web.safestr(v))]
# Raise earlier to avoid SQL query.
if not accounts:
raise web.seeother('/search?msg=SUCCESS')
domains = {v.split('@', 1)[-1] for v in accounts}
_wrap = SQLWrap()
conn = _wrap.conn
# Get managed accounts.
if not session.get('is_global_admin'):
# Get list of managed domains.
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.get('username'),
domain_name_only=True,
listed_only=True)
if qr[0]:
domains = [d for d in domains if d in qr[1]]
accounts = [v for v in accounts if v.split('@', 1)[-1] in domains]
else:
raise web.seeother('/search?msg=%s' % web.urlquote(qr[1]))
if not accounts:
raise web.seeother('/search?msg=SUCCESS')
if action in ['enable']:
qr = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type=account_type,
enable_account=True)
elif action in ['disable']:
qr = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type=account_type,
enable_account=False)
elif action in ['delete']:
keep_mailbox_days = 0 # keep forever
if account_type in ['user', 'domain']:
keep_mailbox_days = form_utils.get_single_value(form=form,
input_name='keep_mailbox_days',
default_value=0,
is_integer=True)
try:
keep_mailbox_days = int(keep_mailbox_days)
except:
if session.get('is_global_admin'):
keep_mailbox_days = 0
else:
_max_days = max(settings.DAYS_TO_KEEP_REMOVED_MAILBOX)
if keep_mailbox_days > _max_days:
# Get the max days
keep_mailbox_days = _max_days
qr = sql_lib_utils.delete_accounts(accounts=accounts,
account_type=account_type,
keep_mailbox_days=keep_mailbox_days,
conn=conn)
else:
raise web.seeother("/search?msg=INVALID_ACTION")
if qr[0]:
raise web.seeother('/search?msg=SUCCESS')
else:
raise web.seeother('/search?msg=%s' % str(qr[1]))
class APILogin:
def GET(self):
return api_render((False, 'INVALID_HTTP_METHOD'))
def POST(self):
"""Login.
curl -X POST -c cookie.txt -d "username=<username>&password=<password>" https://<server>/api/login
Required POST data:
@username - valid email address of domain admin
@password - password of username
"""
if not iredutils.is_allowed_api_client(web.ctx.ip):
return api_render((False, 'NOT_AUTHORIZED'))
# Get username, password.
form = web.input(_unicode=False)
username = form.get('username', '').strip().lower()
password = web.safestr(form.get('password', '').strip())
domain = username.split("@", 1)[-1]
# Auth as domain admin
_wrap = SQLWrap()
conn = _wrap.conn
auth_result = auth.auth(conn=conn,
username=username,
password=password,
account_type='admin')
if auth_result[0]:
log_activity(msg="Admin login success.", domain=domain, event='login')
return api_render(True)
else:
session['failed_times'] += 1
logger.warning("API login failed: client_address={}, username={}".format(web.ctx.ip, username))
log_activity(msg="Admin login failed.",
admin=username,
domain=domain,
event='login',
loglevel='error')
return api_render(auth_result)

368
controllers/sql/domain.py Normal file
View File

@@ -0,0 +1,368 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from libs import iredutils, form_utils
from libs.l10n import TIMEZONES
from libs.sqllib import SQLWrap, decorators, sqlutils
from libs.sqllib import alias as sql_lib_alias
from libs.sqllib import ml as sql_lib_ml
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import admin as sql_lib_admin
from libs.amavisd import spampolicy as spampolicylib, wblist as lib_wblist
from libs.panel.domain_ownership import get_pending_domains
session = web.config.get('_session')
if settings.iredapd_enabled:
from libs.iredapd import throttle as iredapd_throttle
from libs.iredapd import greylist as iredapd_greylist
class List:
@decorators.require_admin_login
def GET(self, cur_page=1, disabled_only=False):
"""List paged mail domains."""
form = web.input(_unicode=False)
cur_page = int(cur_page) or 1
all_domain_profiles = []
domain_used_quota = {}
all_first_chars = []
first_char = None
if 'starts_with' in form:
first_char = form.get('starts_with')[:1].upper()
if not iredutils.is_valid_account_first_char(first_char):
first_char = None
_wrap = SQLWrap()
conn = _wrap.conn
# Get first characters of all domains - no matter whether it's
# requested to list all domains or disabled only.
_qr = sql_lib_domain.get_first_char_of_all_domains(conn=conn)
if _qr[0]:
all_first_chars = _qr[1]
total = sql_lib_admin.num_managed_domains(conn=conn,
disabled_only=disabled_only,
first_char=first_char)
if total:
qr = sql_lib_domain.get_paged_domains(cur_page=cur_page,
first_char=first_char,
disabled_only=disabled_only,
conn=conn)
if qr[0]:
all_domain_profiles = qr[1]
if settings.SHOW_USED_QUOTA:
domains = []
for i in all_domain_profiles:
domains.append(str(i.domain))
domain_used_quota = sql_lib_domain.get_domain_used_quota(conn=conn,
domains=domains)
# Get alias domain names.
all_domain_names = []
all_alias_domains = {}
if all_domain_profiles:
all_domain_names = [str(d.domain).lower() for d in all_domain_profiles]
qr = conn.select('alias_domain',
vars={'all_domain_names': all_domain_names},
what='alias_domain, target_domain',
where='target_domain IN $all_domain_names')
if qr:
for r in qr:
td = str(r.target_domain).lower()
ad = str(r.alias_domain).lower()
if td in all_alias_domains:
all_alias_domains[td].append(ad)
else:
all_alias_domains[td] = [ad]
# Query pending domains which didn't passed ownership verification
pending_domains = []
if all_domain_names:
qr = get_pending_domains(domains=all_domain_names, domain_name_only=True)
if qr[0]:
pending_domains = qr[1]
if session.get('is_global_admin'):
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX_FOR_GLOBAL_ADMIN
else:
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX
return web.render('sql/domain/list.html',
cur_page=cur_page,
total=total,
all_domain_profiles=all_domain_profiles,
all_alias_domains=all_alias_domains,
domain_used_quota=domain_used_quota,
local_transports=settings.LOCAL_TRANSPORTS,
first_char=first_char,
all_first_chars=all_first_chars,
disabled_only=disabled_only,
pending_domains=pending_domains,
days_to_keep_removed_mailbox=days_to_keep_removed_mailbox,
msg=form.get('msg', None))
@decorators.require_admin_login
@decorators.csrf_protected
def POST(self):
form = web.input(domainName=[], _unicode=False)
domains = form.get('domainName', [])
action = form.get('action')
if action not in ['delete', 'enable', 'disable']:
raise web.seeother('/domains?msg=INVALID_ACTION')
_wrap = SQLWrap()
conn = _wrap.conn
if not domains:
raise web.seeother('/domains?msg=INVALID_DOMAIN_NAME')
if session.get('is_global_admin') or session.get('create_new_domains'):
if action == 'delete':
keep_mailbox_days = form_utils.get_single_value(form=form,
input_name='keep_mailbox_days',
default_value=0,
is_integer=True)
qr = sql_lib_domain.delete_domains(domains=domains,
keep_mailbox_days=keep_mailbox_days,
conn=conn)
msg = 'DELETED'
if action in ['enable', 'disable']:
qr = sql_lib_domain.enable_disable_domains(domains=domains,
action=action,
conn=conn)
# msg: ENABLED, DISABLED
msg = action.upper() + 'D'
if qr[0]:
raise web.seeother('/domains?msg=%s' % msg)
else:
raise web.seeother('/domains?msg=' + web.urlquote(qr[1]))
class ListDisabled:
"""List disabled mail domains."""
@decorators.require_admin_login
def GET(self, cur_page=1):
lst = List()
return lst.GET(cur_page=cur_page, disabled_only=True)
class Profile:
@decorators.require_domain_access
def GET(self, profile_type, domain):
form = web.input()
domain = web.safestr(domain.split('/', 1)[0])
profile_type = web.safestr(profile_type)
_wrap = SQLWrap()
conn = _wrap.conn
result = sql_lib_domain.profile(conn=conn, domain=domain)
if result[0] is not True:
raise web.seeother('/domains?msg=' + web.urlquote(result[1]))
domain_profile = result[1]
alias_domains = [] # Get all alias domains.
all_alias_accounts = [] # Get all mail alias accounts.
all_mailing_lists = []
# profile_type == 'throttle'
# throttle: iRedAPD
gl_setting = {}
gl_whitelists = []
inbound_throttle_setting = {}
outbound_throttle_setting = {}
# Get alias domains.
qr = sql_lib_domain.get_all_alias_domains(domain=domain,
name_only=True,
conn=conn)
if qr[0]:
alias_domains = qr[1]
# Get all mail aliases.
mails_of_all_alias_accounts = []
qr = sql_lib_alias.get_basic_alias_profiles(conn=conn,
domain=domain,
columns=['name', 'address'])
if qr[0]:
all_alias_accounts = qr[1]
for ali in all_alias_accounts:
mails_of_all_alias_accounts += [ali.address]
# Get all mailing lists.
mails_of_all_mailing_lists = []
qr = sql_lib_ml.get_basic_ml_profiles(domain=domain,
columns=['address', 'name'],
conn=conn)
if qr[0]:
all_mailing_lists = qr[1]
for i in all_mailing_lists:
mails_of_all_mailing_lists.append(i['address'])
# Get per-admin settings used by normal admin to create new domains.
creation_limits = sql_lib_admin.get_per_admin_domain_creation_limits(admin=session.get('username'), conn=conn)
# Get sender/recipient throttle data from iRedAPD database.
if settings.iredapd_enabled:
_account = '@' + domain
# Greylisting
gl_setting = iredapd_greylist.get_greylist_setting(account=_account)
gl_whitelists = iredapd_greylist.get_greylist_whitelists(account=_account)
# Throttling
inbound_throttle_setting = iredapd_throttle.get_throttle_setting(account=_account,
inout_type='inbound')
outbound_throttle_setting = iredapd_throttle.get_throttle_setting(account=_account,
inout_type='outbound')
spampolicy = {}
global_spam_score = None
if settings.amavisd_enable_policy_lookup:
qr = spampolicylib.get_spam_policy(account='@' + domain)
if not qr[0]:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
spampolicy = qr[1]
global_spam_score = spampolicylib.get_global_spam_score()
# Get per-domain white/blacklists
whitelists = []
blacklists = []
outbound_whitelists = []
outbound_blacklists = []
qr = lib_wblist.get_wblist(account='@' + domain)
if qr[0]:
whitelists = qr[1]['inbound_whitelists']
blacklists = qr[1]['inbound_blacklists']
outbound_whitelists = qr[1]['outbound_whitelists']
outbound_blacklists = qr[1]['outbound_blacklists']
# Domain ownership verification
pending_domains = []
qr = get_pending_domains(domains=[domain], domain_name_only=True)
if qr[0]:
pending_domains = qr[1]
# Get settings from db.
_settings = iredutils.get_settings_from_db(params=['min_passwd_length', 'max_passwd_length'])
global_min_passwd_length = _settings['min_passwd_length']
global_max_passwd_length = _settings['max_passwd_length']
return web.render(
'sql/domain/profile.html',
cur_domain=domain,
profile_type=profile_type,
profile=domain_profile,
default_mta_transport=settings.default_mta_transport,
domain_settings=sqlutils.account_settings_string_to_dict(domain_profile['settings']),
global_min_passwd_length=global_min_passwd_length,
global_max_passwd_length=global_max_passwd_length,
alias_domains=alias_domains,
all_alias_accounts=all_alias_accounts,
mails_of_all_alias_accounts=mails_of_all_alias_accounts,
all_mailing_lists=all_mailing_lists,
mails_of_all_mailing_lists=mails_of_all_mailing_lists,
timezones=TIMEZONES,
creation_limits=creation_limits,
# iRedAPD
gl_setting=gl_setting,
gl_whitelists=gl_whitelists,
inbound_throttle_setting=inbound_throttle_setting,
outbound_throttle_setting=outbound_throttle_setting,
# Language
languagemaps=iredutils.get_language_maps(),
# Spam policy, wblist
spampolicy=spampolicy,
custom_ban_rules=settings.AMAVISD_BAN_RULES,
global_spam_score=global_spam_score,
whitelists=whitelists,
blacklists=blacklists,
outbound_whitelists=outbound_whitelists,
outbound_blacklists=outbound_blacklists,
# domain ownership verification
pending_domains=pending_domains,
msg=form.get('msg'),
)
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, profile_type, domain):
domain = str(domain).lower()
form = web.input(domainAliasName=[],
domainAdmin=[],
default_mail_list=[],
defaultList=[],
enabledService=[],
disabledMailService=[],
disabledDomainProfile=[],
disabledUserProfile=[],
disabledUserPreference=[],
banned_rulenames=[])
result = sql_lib_domain.update(profile_type=profile_type,
domain=domain,
form=form)
if result[0]:
raise web.seeother('/profile/domain/{}/{}?msg=UPDATED'.format(profile_type, domain))
else:
raise web.seeother('/profile/domain/{}/{}?msg={}'.format(profile_type, domain, web.urlquote(result[1])))
class Create:
@decorators.require_permission_create_domain
def GET(self):
form = web.input()
admin = session.get('username')
# for normal domain admin: check limitations
creation_limits = sql_lib_admin.get_per_admin_domain_creation_limits(admin=admin)
if creation_limits['error_code']:
msg = None
else:
msg = form.get('msg')
return web.render('sql/domain/create.html',
preferred_language=settings.default_language,
languagemaps=iredutils.get_language_maps(),
timezones=TIMEZONES,
creation_limits=creation_limits,
msg=msg)
@decorators.require_permission_create_domain
@decorators.csrf_protected
def POST(self):
form = web.input()
domain = form_utils.get_domain_name(form)
result = sql_lib_domain.add(form=form)
if result[0]:
raise web.seeother('/profile/domain/general/%s?msg=CREATED' % domain)
else:
raise web.seeother('/create/domain?msg=%s' % web.urlquote(result[1]))

257
controllers/sql/export.py Normal file
View File

@@ -0,0 +1,257 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import zipfile
import io
import csv
import web
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import general as sql_lib_general
from libs.sqllib import admin as sql_lib_admin
session = web.config.get('_session')
class ExportManagedAccounts:
@decorators.require_admin_login
def GET(self, mail):
mail = mail.lower()
# Raise error if normal admin is trying to export accounts managed by
# other admin
if (not session.get('is_global_admin')) and session.get('username') != mail:
raise web.seeother('/domains?msg=PERMISSION_DENIED')
qr = sql_lib_general.export_managed_accounts(mail=mail, domains=None, conn=None)
if not qr[0]:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
managed_domains = qr[1]
# Generate summary
content_summary = ['Accounts managed by admin: {}'.format(mail), '------']
_domains = []
_total_domains = 0
_total_users = 0
_total_lists = 0
_total_aliases = 0
for d in managed_domains:
_total_domains += 1
_domains += [d['domain']]
_total_users += d['total_users']
_total_lists += d['total_lists']
_total_aliases += d['total_aliases']
content_summary += ['- Domains: {}'.format(_total_domains)]
content_summary += ['- Mailboxes: {}'.format(_total_users)]
content_summary += ['- Mailing lists: {}'.format(_total_lists)]
content_summary += ['- Mail aliases: {}'.format(_total_aliases)]
# Generate zip file
f = io.BytesIO()
try:
zf = zipfile.ZipFile(f, mode='w', compression=zipfile.ZIP_DEFLATED)
# Summary of all managed accounts
zf.writestr('summary.txt', '\n'.join(content_summary))
_content_domains = ['# Exported domains:']
_content_domains += ['# Format: domain name, display name']
# Generate files for each domain
for d in managed_domains:
_domain = d['domain']
_content_domains += ['{domain}, {name}'.format(**d)]
for _account_type in ['users', 'lists', 'aliases']:
if d['total_' + _account_type] == 0:
continue
if _account_type == 'users':
_content = ['# Mailboxes under domain %s' % _domain]
elif _account_type == 'lists':
_content = ['# Mailing lists under domain %s' % _domain]
else:
# account_type == 'aliases'
_content = ['# Mail aliases under domain %s' % _domain]
_content += ['# Format: mail address, display name']
for _account in d[_account_type]:
_content += ['{mail}, {name}'.format(**_account)]
zf.writestr(_domain + '_' + _account_type + '.txt', '\n'.join(_content))
zf.writestr('domains.txt', '\n'.join(_content_domains))
except Exception as e:
raise web.seeother('/domains?msg=%s' % web.urlquote(repr(e)))
finally:
zf.close()
web.header('Content-Disposition', 'attachment; filename=accounts.zip')
return f.getvalue()
class ExportDomainAccounts:
@decorators.require_admin_login
def GET(self, domain):
domain = str(domain).lower()
mail = session.get('username')
_wrap = SQLWrap()
conn = _wrap.conn
if not sql_lib_general.is_domain_admin(domain=domain, admin=mail, conn=conn):
raise web.seeother('/domains?msg=PERMISSION_DENIED')
qr = sql_lib_general.export_managed_accounts(mail=mail, domains=[domain], conn=conn)
if not qr[0]:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
managed_domains = qr[1]
_domains = []
_total_domains = 0
_total_users = 0
_total_lists = 0
_total_aliases = 0
for d in managed_domains:
_total_domains += 1
_domains += [d['domain']]
_total_users += d['total_users']
_total_lists += d['total_lists']
_total_aliases += d['total_aliases']
# Generate zip file
f = io.BytesIO()
with zipfile.ZipFile(f, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
# Summary of all managed accounts
content_summary = ['- Exported domains: %d' % _total_domains]
content_summary += ['- Mailboxes: %d' % _total_users]
content_summary += ['- Mailing lists: %d' % _total_lists]
content_summary += ['- Mail aliases: %d' % _total_aliases]
zf.writestr('summary.txt', '\n'.join(content_summary))
_content_domains = ['# All managed domains:']
_content_domains += ['# Format: domain name, display name']
# Generate files for each domain
for d in managed_domains:
_domain = d['domain']
_content_domains += ['{domain}, {name}'.format(**d)]
for account_type in ['users', 'lists', 'aliases']:
if account_type == 'users':
_content = ['# Mailboxes under domain %s' % _domain]
elif account_type == 'lists':
_content = ['# Mailing lists under domain %s' % _domain]
else:
# account_type == 'aliases'
_content = ['# Mail aliases under domain %s' % _domain]
_content += ['# Format: mail address, display name']
for _account in d[account_type]:
_content += ['{mail}, {name}'.format(**_account)]
zf.writestr(_domain + '_' + account_type + '.txt', '\n'.join(_content))
zf.writestr('domains.txt', '\n'.join(_content_domains))
web.header('Content-Disposition', 'attachment; filename=accounts.zip')
return f.getvalue()
class ExportAdminStatistics:
@decorators.require_global_admin
def GET(self):
"""
Admin <email>
domain1.com | 12 Mailboxes | 3 Mailinglists
domain2.com | 9 Mailboxes | 1 Mailinglist
"""
_wrap = SQLWrap()
conn = _wrap.conn
# Get all admins
qr = sql_lib_admin.get_all_admins(email_only=True, conn=conn)
if not qr[0]:
return qr
all_admins = qr[1]
# Get all global admins
qr = sql_lib_admin.get_all_global_admins(conn=conn)
if not qr[0]:
return qr
global_admins = qr[1]
non_global_admins = [i for i in all_admins if i not in global_admins]
# dict used to store analyzed domain names to avoid duplicate ldap query:
# {'<domain>': {'user': 10,
# 'aliases': 23,
# 'maillists': 2}, ...}
_analyzed_domains = {}
# dict used to store admin and managed domains.
# {'<admin-email>': [<domain>, <domain>, ...], ...}
# WARNING: it's possible that admin doesn't manage any domains.
_admins_and_domains = {}
# Write statistics in csv file.
for _admin in non_global_admins:
_qr = sql_lib_admin.get_managed_domains(admin=_admin,
domain_name_only=True,
listed_only=True,
conn=conn)
if _qr[0]:
_domains = _qr[1]
_admins_and_domains[_admin] = _domains
for _domain in _domains:
if _domain not in _analyzed_domains:
_num_users = sql_lib_general.num_users_under_domain(domain=_domain, conn=conn)
_num_aliases = sql_lib_general.num_aliases_under_domain(domain=_domain, conn=conn)
_num_ml = sql_lib_general.num_maillists_under_domain(domain=_domain, conn=conn)
_analyzed_domains[_domain] = {'users': _num_users,
'aliases': _num_aliases,
'maillists': _num_ml}
_rows = []
for _admin in global_admins:
_rows.append([_admin, 'ALL'])
for (_admin, _domains) in list(_admins_and_domains.items()):
_rows.append([_admin, len(_domains)])
_count = 1
for _domain in _domains:
_num_users = _analyzed_domains[_domain]['users']
_num_aliases = _analyzed_domains[_domain]['aliases']
_num_maillists = _analyzed_domains[_domain]['maillists']
_rows.append([_count, _domain, _num_users, _num_aliases, _num_maillists])
_count += 1
try:
f = io.StringIO()
cw = csv.writer(f)
# Header row
cw.writerow(['Admin', 'Managed Domains', 'Users', 'Aliases', 'Mailing Lists'])
# Data rows
cw.writerows(_rows)
v = f.getvalue()
except Exception as e:
raise web.seeother('/domains?msg=%s' % web.urlquote(repr(e)))
web.header('Content-Disposition', 'attachment; filename=statistics_admins.csv')
return v

376
controllers/sql/ml.py Normal file
View File

@@ -0,0 +1,376 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
from libs import iredutils, form_utils
from libs.sqllib import SQLWrap, decorators
from libs.sqllib import ml as sql_lib_ml
from libs.sqllib import admin as sql_lib_admin
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import general as sql_lib_general
from libs.sqllib import utils as sql_lib_utils
session = web.config.get('_session')
class List:
@decorators.require_domain_access
def GET(self, domain, cur_page=1, disabled_only=False):
domain = str(domain).lower()
cur_page = int(cur_page) or 1
form = web.input(_unicode=False)
all_first_chars = []
first_char = None
if 'starts_with' in form:
first_char = form.get('starts_with')[:1].upper()
if not iredutils.is_valid_account_first_char(first_char):
first_char = None
_wrap = SQLWrap()
conn = _wrap.conn
total = sql_lib_ml.num_maillists_under_domain(conn=conn,
domain=domain,
disabled_only=disabled_only,
first_char=first_char)
records = []
if total:
_qr = sql_lib_general.get_first_char_of_all_accounts(domain=domain,
account_type='ml',
conn=conn)
if _qr[0]:
all_first_chars = _qr[1]
qr = sql_lib_ml.get_basic_ml_profiles(conn=conn,
domain=domain,
page=cur_page,
first_char=first_char,
disabled_only=disabled_only)
if qr[0]:
records = qr[1]
return web.render(
'sql/ml/list.html',
cur_domain=domain,
cur_page=cur_page,
total=total,
maillists=records,
all_first_chars=all_first_chars,
first_char=first_char,
msg=form.get('msg', None),
)
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, domain):
form = web.input(_unicode=False, mail=[])
domain = str(domain).lower()
accounts = form.get('mail', [])
action = form.get('action', None)
msg = form.get('msg', None)
# Filter aliases not under the same domain.
accounts = [str(v).lower()
for v in accounts
if iredutils.is_email(v) and str(v).endswith('@' + domain)]
_wrap = SQLWrap()
conn = _wrap.conn
if action == 'delete':
result = sql_lib_ml.delete_maillists(accounts=accounts,
keep_archive=True,
conn=conn)
msg = 'DELETED'
elif action == 'delete_without_archiving':
result = sql_lib_ml.delete_maillists(accounts=accounts,
keep_archive=False,
conn=conn)
msg = 'DELETED'
elif action == 'disable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type='maillist',
enable_account=False)
msg = 'DISABLED'
elif action == 'enable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=accounts,
account_type='maillist',
enable_account=True)
msg = 'ENABLED'
else:
result = (False, 'INVALID_ACTION')
if result[0]:
raise web.seeother('/mls/{}?msg={}'.format(domain, msg))
else:
raise web.seeother('/mls/{}?msg={}'.format(domain, web.urlquote(result[1])))
class Create:
@decorators.require_domain_access
def GET(self, domain):
domain = str(domain).lower()
form = web.input()
all_domains = []
# Get all domains, select the first one.
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.get('username'),
domain_name_only=True)
if qr[0]:
all_domains = qr[1]
# Get domain profile.
qr_profile = sql_lib_domain.simple_profile(domain=domain, conn=conn)
if qr_profile[0]:
domain_profile = qr_profile[1]
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr_profile[1]))
# Get total number and allocated quota size of existing users under domain.
num_maillists_under_domain = sql_lib_ml.num_maillists_under_domain(domain=domain, conn=conn)
# TODO read default creation settings from domain profile.
# Default creation settings
default_creation_settings = {'only_subscriber_can_post': 'yes'}
return web.render(
'sql/ml/create.html',
cur_domain=domain,
allDomains=all_domains,
profile=domain_profile,
num_existing_maillists=num_maillists_under_domain,
default_creation_settings=default_creation_settings,
msg=form.get('msg'),
)
@decorators.require_domain_access
@decorators.csrf_protected
def POST(self, domain):
domain = str(domain).lower()
form = web.input()
domain_in_form = form_utils.get_domain_name(form)
if domain != domain_in_form:
raise web.seeother('/domains?msg=PERMISSION_DENIED')
listname = form_utils.get_single_value(form, input_name='listname', to_string=True, to_lowercase=True)
mail = listname + '@' + domain
qr = sql_lib_ml.add_ml_from_web_form(domain=domain, form=form)
if qr[0]:
raise web.seeother('/profile/ml/general/%s?msg=CREATED' % mail)
else:
raise web.seeother('/create/ml/{}?msg={}'.format(domain, web.urlquote(qr[1])))
class Profile:
@decorators.require_domain_access
def GET(self, profile_type, mail):
form = web.input()
mail = str(mail).lower()
domain = mail.split('@', 1)[-1]
_wrap = SQLWrap()
conn = _wrap.conn
# Get mlmmj account profile
qr = sql_lib_ml.get_profile(mail=mail, conn=conn)
if qr[0] is not True:
raise web.seeother('/mls/{}?msg={}'.format(domain, web.urlquote(qr[1])))
profile = qr[1]
# Get per-account alias addresses.
qr = sql_lib_ml.get_alias_addresses(mail=mail, conn=conn)
if qr[0]:
alias_addresses = qr[1]
else:
raise web.seeother('/mls/{}?msg={}'.format(domain, web.urlquote(qr[1])))
# Get subscribers
subscribers = []
qr = sql_lib_ml.get_subscribers(mail=mail)
if qr[0]:
subscribers = qr[1]
return web.render('sql/ml/profile.html',
cur_domain=domain,
mail=mail,
profile_type=profile_type,
profile=profile,
alias_addresses=alias_addresses,
subscribers=subscribers,
msg=form.get('msg'))
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, profile_type, mail):
form = web.input(subscriber=[])
result = sql_lib_ml.update(mail=mail,
profile_type=profile_type,
form=form)
if result[0]:
raise web.seeother('/profile/ml/{}/{}?msg=UPDATED'.format(profile_type, mail))
else:
raise web.seeother('/profile/ml/{}/{}?msg={}'.format(profile_type, mail, web.urlquote(result[1])))
class AddSubscribers:
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, mail):
form = web.input(_unicode=False)
_require_confirm = 'require_confirm' in form
qr = sql_lib_ml.add_subscribers(mail=mail, form=form)
if qr[0]:
if _require_confirm:
raise web.seeother('/profile/ml/members/%s?msg=CONFIRM_MAIL_SENT' % mail)
else:
raise web.seeother('/profile/ml/members/%s?msg=MEMBERS_ADDED' % mail)
else:
raise web.seeother('/profile/ml/members/{}?msg={}'.format(mail, web.urlquote(qr[1])))
class MigrateAliasToML:
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, mail):
mail = str(mail).lower()
domain = mail.split('@', 1)[-1]
qr = sql_lib_ml.migrate_alias_to_ml(mail=mail)
if qr[0]:
raise web.seeother('/profile/ml/general/%s?msg=MIGRATED' % mail)
else:
raise web.seeother('/aliases/{}?msg={}'.format(domain, web.urlquote(qr[1])))
# self-service: allow user to manage lists as owner or moderator.
class ManagedMls:
@decorators.require_preference_access("manageml")
def GET(self, cur_page=1):
mail = session['username']
cur_page = int(cur_page) or 1
form = web.input(_unicode=False)
all_first_chars = []
first_char = None
if 'starts_with' in form:
first_char = form.get('starts_with')[:1].upper()
if not iredutils.is_valid_account_first_char(first_char):
first_char = None
_wrap = SQLWrap()
conn = _wrap.conn
# Get managed mailing lists.
total = sql_lib_ml.num_maillists_managed_by_user(mail=mail, first_char=first_char, conn=conn)
rows = []
if total:
_qr = sql_lib_ml.get_first_char_of_all_managed_mls(mail=mail, conn=conn)
if _qr[0]:
all_first_chars = _qr[1]
qr = sql_lib_ml.get_basic_profiles_of_managed_mls(
page=cur_page,
first_char=first_char,
conn=conn,
)
if qr[0]:
rows = qr[1]
return web.render(
'sql/self-service/ml/list.html',
cur_page=cur_page,
total=total,
maillists=rows,
all_first_chars=all_first_chars,
first_char=first_char,
msg=form.get('msg', None),
)
class ManagedMlProfile:
@decorators.require_preference_access("manageml")
@decorators.require_ml_owner_or_moderator
def GET(self, profile_type, mail):
form = web.input()
mail = str(mail).lower()
domain = mail.split('@', 1)[-1]
_wrap = SQLWrap()
conn = _wrap.conn
# Get account profile
qr = sql_lib_ml.get_profile(mail=mail, conn=conn)
if not qr[0]:
raise web.seeother('/mls/{}?msg={}'.format(domain, web.urlquote(qr[1])))
profile = qr[1]
# Get subscribers
subscribers = []
qr = sql_lib_ml.get_subscribers(mail=mail)
if qr[0]:
subscribers = qr[1]
return web.render('sql/self-service/ml/profile.html',
mail=mail,
profile_type=profile_type,
profile=profile,
subscribers=subscribers,
msg=form.get('msg'))
@decorators.require_preference_access("manageml")
@decorators.csrf_protected
@decorators.require_ml_owner_or_moderator
def POST(self, profile_type, mail):
form = web.input(subscriber=[])
qr = sql_lib_ml.update(mail=mail,
profile_type=profile_type,
form=form)
if qr[0]:
raise web.seeother('/self-service/ml/profile/{}/{}?msg=UPDATED'.format(profile_type, mail))
else:
raise web.seeother('/self-service/ml/profile/{}/{}?msg={}'.format(profile_type, mail, web.urlquote(qr[1])))
# self-service
class ManagedMlAddSubscribers:
@decorators.require_preference_access("manageml")
@decorators.csrf_protected
@decorators.require_ml_owner_or_moderator
def POST(self, mail):
form = web.input(_unicode=False)
qr = sql_lib_ml.add_subscribers(mail=mail, form=form)
if qr[0]:
raise web.seeother('/self-service/ml/profile/members/%s?msg=MEMBERS_ADDED' % mail)
else:
raise web.seeother('/self-service/ml/profile/members/{}?msg={}'.format(mail, web.urlquote(qr[1])))

169
controllers/sql/urls.py Normal file
View File

@@ -0,0 +1,169 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import settings
from libs.regxes import email as e, domain as d
# fmt: off
urls = [
# Make url ending with or without '/' going to the same class.
'/(.*)/', 'controllers.utils.Redirect',
'/', 'controllers.sql.basic.Login',
'/login', 'controllers.sql.basic.Login',
'/logout', 'controllers.sql.basic.Logout',
'/dashboard', 'controllers.sql.basic.Dashboard',
# Search.
'/search', 'controllers.sql.basic.Search',
# Perform some operations from search page.
'/action/(user|alias|ml)', 'controllers.sql.basic.OperationsFromSearchPage',
# Export managed accounts
'/export/managed_accounts/(%s$)' % e, 'controllers.sql.export.ExportManagedAccounts',
'/export/statistics/admins', 'controllers.sql.export.ExportAdminStatistics',
'/export/domain/(%s$)' % d, 'controllers.sql.export.ExportDomainAccounts',
# Domain related.
'/domains', 'controllers.sql.domain.List',
r'/domains/page/(\d+)', 'controllers.sql.domain.List',
# List disabled accounts.
'/domains/disabled', 'controllers.sql.domain.ListDisabled',
r'/domains/disabled/page/(\d+)', 'controllers.sql.domain.ListDisabled',
# Domain profiles
'/profile/domain/(general)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(aliases)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(relay)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(backupmx)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(bcc)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(catchall)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(throttle)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(greylisting)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(wblist)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(spampolicy)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(advanced)/(%s$)' % d, 'controllers.sql.domain.Profile',
'/profile/domain/(%s)' % d, 'controllers.sql.domain.Profile',
'/create/domain', 'controllers.sql.domain.Create',
# Admin related.
'/admins', 'controllers.sql.admin.List',
r'/admins/page/(\d+)', 'controllers.sql.admin.List',
'/profile/admin/(general)/(%s$)' % e, 'controllers.sql.admin.Profile',
'/profile/admin/(password)/(%s$)' % e, 'controllers.sql.admin.Profile',
'/create/admin', 'controllers.sql.admin.Create',
# Redirect to first mail domain.
'/create/(user|ml|alias)', 'controllers.sql.utils.CreateDispatcher',
# User related.
'/users/(%s$)' % d, 'controllers.sql.user.List',
r'/users/(%s)/page/(\d+)' % d, 'controllers.sql.user.List',
# List disabled accounts.
'/users/(%s)/disabled' % d, 'controllers.sql.user.ListDisabled',
r'/users/(%s)/disabled/page/(\d+)' % d, 'controllers.sql.user.ListDisabled',
# List all last logins.
'/users/(%s)/last_logins' % d, 'controllers.sql.user.AllLastLogins',
# Create user.
'/create/user/(%s$)' % d, 'controllers.sql.user.Create',
# Profile pages.
'/profile/user/(general)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(forwarding)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(bcc)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(relay)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(aliases)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(wblist)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(spampolicy)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(password)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(throttle)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(greylisting)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(advanced)/(%s$)' % e, 'controllers.sql.user.Profile',
'/profile/user/(rename)/(%s$)' % e, 'controllers.sql.user.Profile',
'/apiproxy/user/(%s$)' % e, 'controllers.sql.user.APIProxyUser',
####################
# mlmmj mailing list
#
'/create/ml/(%s$)' % d, 'controllers.sql.ml.Create',
# make it compatible with old (LDAP) mailing list
'/create/maillist/(%s$)' % d, 'controllers.sql.ml.Create',
'/mls/(%s$)' % d, 'controllers.sql.ml.List',
r'/mls/(%s)/page/(\d+)' % d, 'controllers.sql.ml.List',
'/profile/ml/(general|aliases|owners|members|newsletter)/(%s$)' % e, 'controllers.sql.ml.Profile',
# Add subscribers
'/profile/ml/add_subscribers/(%s$)' % e, 'controllers.sql.ml.AddSubscribers',
# migrate alias account to mlmmj mailing list.
'/migrate/alias_to_ml/(%s$)' % e, 'controllers.sql.ml.MigrateAliasToML',
# Alias related.
'/aliases', 'controllers.sql.alias.List',
'/aliases/(%s$)' % d, 'controllers.sql.alias.List',
r'/aliases/(%s)/page/(\d+)' % d, 'controllers.sql.alias.List',
# List disabled accounts.
'/aliases/(%s)/disabled' % d, 'controllers.sql.alias.ListDisabled',
r'/aliases/(%s)/disabled/page/(\d+)' % d, 'controllers.sql.alias.ListDisabled',
'/profile/alias/(general)/(%s$)' % e, 'controllers.sql.alias.Profile',
'/profile/alias/(members)/(%s$)' % e, 'controllers.sql.alias.Profile',
'/profile/alias/(rename)/(%s$)' % e, 'controllers.sql.alias.Profile',
'/create/alias/(%s$)' % d, 'controllers.sql.alias.Create',
# User admins
'/admins/(%s$)' % d, 'controllers.sql.user.Admin',
r'/admins/(%s)/page/(\d+)' % d, 'controllers.sql.user.Admin',
#
# Self-service
#
'/preferences', 'controllers.sql.user.Preferences',
'/preferences/(general)$', 'controllers.sql.user.Preferences',
'/preferences/(forwarding)$', 'controllers.sql.user.Preferences',
'/preferences/(password)$', 'controllers.sql.user.Preferences',
# manage owned or moderated mailing lists
'/self-service/mls', 'controllers.sql.ml.ManagedMls',
'/self-service/mls/page/(\d+)', 'controllers.sql.ml.ManagedMls',
'/self-service/ml/profile/(general|owners|members|newsletter)/(%s$)' % e, 'controllers.sql.ml.ManagedMlProfile',
'/self-service/ml/profile/add_subscribers/(%s$)' % e, 'controllers.sql.ml.ManagedMlAddSubscribers',
]
# API Interfaces
if settings.ENABLE_RESTFUL_API:
urls += [
# API Interfaces
'/api/login', 'controllers.sql.basic.APILogin',
#
# Domain
#
'/api/domains', 'controllers.sql.api_domain.APIDomains',
'/api/domain/(%s$)' % d, 'controllers.sql.api_domain.APIDomain',
# Delete domain, and keep mailboxes for given days
r'/api/domain/(%s)/keep_mailbox_days/(\d+)' % d, 'controllers.sql.api_domain.APIDomain',
'/api/domain/admins/(%s$)' % d, 'controllers.sql.api_domain.APIDomainAdmin',
# User
'/api/user/(%s$)' % e, 'controllers.sql.api_user.APIUser',
# Delete user, and keep mailboxes for given days
r'/api/user/(%s)/keep_mailbox_days/(\d+)' % e, 'controllers.sql.api_user.APIUser',
'/api/user/({})/change_email/({}$)'.format(e, e), 'controllers.sql.api_user.APIChangeEmail',
'/api/users/(%s$)' % d, 'controllers.sql.api_user.APIUsers',
# Alias
'/api/alias/(%s$)' % e, 'controllers.sql.api_alias.APIAlias',
'/api/alias/({})/change_email/({}$)'.format(e, e), 'controllers.sql.api_alias.APIChangeEmail',
'/api/aliases/(%s$)' % d, 'controllers.sql.api_alias.APIAliases',
# (mlmmj) mailing list
'/api/mls/(%s$)' % d, 'controllers.sql.api_ml.APIMLS',
'/api/ml/(%s$)' % e, 'controllers.sql.api_ml.APIML',
# Admin
'/api/admin/(%s$)' % e, 'controllers.sql.api_admin.APIAdmin',
#
# Misc
#
# Verify account password.
'/api/verify_password/(user)/(%s$)' % e, 'controllers.sql.api_misc.APIVerifyPassword',
'/api/verify_password/(admin)/(%s$)' % e, 'controllers.sql.api_misc.APIVerifyPassword',
]
# fmt: on

834
controllers/sql/user.py Normal file
View File

@@ -0,0 +1,834 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import web
import settings
from controllers.utils import api_render
from libs import iredutils, form_utils
from libs.l10n import TIMEZONES
from libs.sqllib import SQLWrap, decorators, sqlutils
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 admin as sql_lib_admin
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import utils as sql_lib_utils
from libs.sqllib import general as sql_lib_general
from libs import mlmmj
from libs.amavisd import spampolicy as spampolicylib, wblist as lib_wblist
session = web.config.get('_session')
if settings.iredapd_enabled:
from libs.iredapd import throttle as iredapd_throttle
from libs.iredapd import greylist as iredapd_greylist
class List:
@decorators.require_domain_access
def GET(self, domain, cur_page=1, disabled_only=False):
domain = str(domain).lower()
cur_page = int(cur_page) or 1
form = web.input(_unicode=False)
order_name = form.get('order_name')
order_by_desc = (form.get('order_by', 'asc').lower() == 'desc')
records = []
# Real-time used quota.
used_quotas = {}
# Last login date
last_logins = {}
# Forwardings and per-user alias addresses
user_forwardings = {}
user_alias_addresses = {}
user_assigned_groups = {}
all_first_chars = []
first_char = None
if 'starts_with' in form:
first_char = form.get('starts_with')[:1].upper()
if not iredutils.is_valid_account_first_char(first_char):
first_char = None
_wrap = SQLWrap()
conn = _wrap.conn
total = sql_lib_user.num_users_under_domains(conn=conn,
domains=[domain],
disabled_only=disabled_only,
first_char=first_char)
if total:
_qr = sql_lib_general.get_first_char_of_all_accounts(domain=domain,
account_type='user',
conn=conn)
if _qr[0]:
all_first_chars = _qr[1]
qr = sql_lib_user.get_paged_users(conn=conn,
domain=domain,
cur_page=cur_page,
order_name=order_name,
order_by_desc=order_by_desc,
first_char=first_char,
disabled_only=disabled_only)
if qr[0]:
records = qr[1]
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
# Get list of email addresses
mails = []
for r in records:
mails += [str(r.get('username')).lower()]
if mails:
# Get real-time mailbox usage
if settings.SHOW_USED_QUOTA:
try:
used_quotas = sql_lib_general.get_account_used_quota(accounts=mails, conn=conn)
except Exception:
pass
# Get last login
last_logins = sql_lib_general.get_account_last_login(accounts=mails, conn=conn)
# Get user forwardings
(_status, _result) = sql_lib_user.get_bulk_user_forwardings(conn=conn, mails=mails)
if _status:
user_forwardings = _result
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
# Get user alias addresses
(_status, _result) = sql_lib_user.get_bulk_user_alias_addresses(mails=mails, conn=conn)
if _status:
user_alias_addresses = _result
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
# Get assigned groups
(_status, _result) = sql_lib_user.get_bulk_user_assigned_groups(mails=mails, conn=conn)
if _status:
user_assigned_groups = _result
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
if session.get('is_global_admin'):
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX_FOR_GLOBAL_ADMIN
else:
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX
return web.render('sql/user/list.html',
cur_domain=domain,
cur_page=cur_page,
total=total,
users=records,
user_forwardings=user_forwardings,
user_alias_addresses=user_alias_addresses,
user_assigned_groups=user_assigned_groups,
used_quotas=used_quotas,
last_logins=last_logins,
order_name=order_name,
order_by_desc=order_by_desc,
all_first_chars=all_first_chars,
first_char=first_char,
disabled_only=disabled_only,
days_to_keep_removed_mailbox=days_to_keep_removed_mailbox,
msg=form.get('msg', None))
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, domain, page=1):
form = web.input(_unicode=False, mail=[])
page = int(page)
if page < 1:
page = 1
domain = str(domain).lower()
# Filter users not under the same domain.
mails = [str(v).strip().lower() for v in form.get("mail", [])]
mails = [v for v in mails if iredutils.is_email(v) and v.endswith('@' + domain)]
action = form.get('action', None)
msg = form.get('msg', None)
redirect_to_admin_list = False
if 'redirect_to_admin_list' in form:
redirect_to_admin_list = True
_wrap = SQLWrap()
conn = _wrap.conn
if action == 'delete':
keep_mailbox_days = form_utils.get_single_value(form=form,
input_name='keep_mailbox_days',
default_value=0,
is_integer=True)
result = sql_lib_user.delete_users(conn=conn,
accounts=mails,
keep_mailbox_days=keep_mailbox_days)
msg = 'DELETED'
elif action == 'disable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=mails,
account_type='user',
enable_account=False)
msg = 'DISABLED'
elif action == 'enable':
result = sql_lib_utils.set_account_status(conn=conn,
accounts=mails,
account_type='user',
enable_account=True)
msg = 'ENABLED'
elif action == 'markasadmin':
result = sql_lib_user.mark_user_as_admin(conn=conn,
domain=domain,
users=mails,
as_normal_admin=True)
msg = 'MARKASADMIN'
elif action == 'unmarkasadmin':
result = sql_lib_user.mark_user_as_admin(conn=conn,
domain=domain,
users=mails,
as_normal_admin=False)
msg = 'UNMARKASADMIN'
elif action == 'markasglobaladmin':
result = sql_lib_user.mark_user_as_admin(conn=conn,
domain=domain,
users=mails,
as_global_admin=True)
msg = 'MARKASGLOBALADMIN'
elif action == 'unmarkasglobaladmin':
result = sql_lib_user.mark_user_as_admin(conn=conn,
domain=domain,
users=mails,
as_global_admin=False)
msg = 'UNMARKASGLOBALADMIN'
else:
result = (False, 'INVALID_ACTION')
if result[0]:
if redirect_to_admin_list:
raise web.seeother('/admins/%s/page/%d?msg=%s' % (domain, page, msg))
else:
raise web.seeother('/users/%s/page/%d?msg=%s' % (domain, page, msg))
else:
if redirect_to_admin_list:
raise web.seeother('/admins/%s/page/%d?msg=%s' % (domain, page, web.urlquote(result[1])))
else:
raise web.seeother('/users/%s/page/%d?msg=%s' % (domain, page, web.urlquote(result[1])))
class ListDisabled:
@decorators.require_domain_access
def GET(self, domain, cur_page=1):
_instance = List()
return _instance.GET(domain=domain, cur_page=cur_page, disabled_only=True)
class Profile:
# Don't use decorator `@decorators.require_domain_access` here, because if
# domain admin doesn't manage its own domain, it cannot access its own
# profile.
def GET(self, profile_type, mail):
mail = str(mail).lower()
domain = mail.split('@', 1)[-1]
_wrap = SQLWrap()
conn = _wrap.conn
# - Allow global admin
# - normal admin who manages this domain
# - allow normal admin who doesn't manage this domain, but is updating its own profile
if sql_lib_general.is_domain_admin(domain=domain, admin=session.get('username'), conn=conn) or \
(session.get('is_normal_admin') and session.get('username') == mail):
pass
else:
raise web.seeother('/domains?msg=PERMISSION_DENIED')
if profile_type == 'rename':
raise web.seeother('/profile/user/general/' + mail)
form = web.input()
msg = form.get('msg', '')
discarded_aliases = form.get('discarded_aliases', '')
if discarded_aliases:
discarded_aliases = [i.strip().lower()
for i in discarded_aliases.split(',')]
# profile_type == 'general'
used_quota = {}
last_logins = {}
# profile_type == 'greylisting'
# greylisting: iRedAPD
gl_setting = {}
gl_whitelists = []
# profile_type == 'throttle'
# throttle: iRedAPD
inbound_throttle_setting = {}
outbound_throttle_setting = {}
# profile_type == 'advanced'
disabled_user_profiles = [] # Per-domain disabled user profiles.
if mail.startswith('@') and iredutils.is_domain(domain):
# Catchall account.
raise web.seeother('/profile/domain/catchall/%s' % domain)
qr = sql_lib_user.profile(mail=mail, conn=conn)
if qr[0]:
user_profile = qr[1]
if not session.get('is_global_admin'):
sql_lib_user.redirect_if_user_is_global_admin(conn=conn, mail=mail, user_profile=user_profile)
else:
raise web.seeother('/users/{}?msg={}'.format(domain, web.urlquote(qr[1])))
del qr
# Get mailbox.allow_nets
allow_nets = []
_allow_nets = user_profile.get('allow_nets')
if _allow_nets:
allow_nets = _allow_nets.split(',')
# Get per-user settings
user_settings = {}
qr = sql_lib_general.get_user_settings(conn=conn,
mail=mail,
existing_settings=user_profile['settings'])
if qr[0]:
user_settings = qr[1]
del qr
# Get used quota.
if settings.SHOW_USED_QUOTA:
used_quota = sql_lib_general.get_account_used_quota(accounts=[mail], conn=conn)
# Get last login.
last_logins = sql_lib_general.get_account_last_login(accounts=[mail], conn=conn)
# Get basic profile of all mail alias accounts under same domain.
all_aliases = []
(_status, _result) = sql_lib_alias.get_basic_alias_profiles(domain=domain, conn=conn)
if _status:
all_aliases = _result
# Get email addresses of mail alias accounts which has current mail
# user as a member
assigned_aliases = []
(_status, _result) = sql_lib_user.get_assigned_aliases(mail=mail, conn=conn)
if _status:
assigned_aliases = _result
# Get per-user alias addresses.
user_alias_addresses = []
qr = sql_lib_user.get_user_alias_addresses(mail=mail, conn=conn)
if qr[0]:
user_alias_addresses = qr[1]
# subscribable mailing lists
all_maillist_addresses = []
all_subscribed_lists = []
_qr = sql_lib_ml.get_basic_ml_profiles(domain=domain,
columns=['address', 'name'],
conn=conn)
if _qr[0]:
all_maillist_profiles = _qr[1]
for i in all_maillist_profiles:
all_maillist_addresses.append(i['address'])
else:
return _qr
# Get subscribed mailing lists
_qr = mlmmj.get_subscribed_lists(mail=mail, query_all_lists=False)
if _qr[0]:
for i in _qr[1]:
all_subscribed_lists.append(i['mail'])
# Get per-domain disabled user profiles.
qr = sql_lib_domain.simple_profile(conn=conn,
domain=domain,
columns=['settings'])
if qr[0]:
domain_profile = qr[1]
domain_settings = sqlutils.account_settings_string_to_dict(domain_profile['settings'])
disabled_user_profiles = domain_settings.get('disabled_user_profiles', [])
db_settings = iredutils.get_settings_from_db()
_min_passwd_length = db_settings['min_passwd_length']
_max_passwd_length = db_settings['max_passwd_length']
min_passwd_length = domain_settings.get('min_passwd_length', _min_passwd_length)
max_passwd_length = domain_settings.get('max_passwd_length', _max_passwd_length)
# Get sender dependent relayhost
relayhost = ''
(_status, _result) = sql_lib_general.get_sender_relayhost(sender=mail, conn=conn)
if _status:
relayhost = _result
if settings.iredapd_enabled:
# Greylisting
gl_setting = iredapd_greylist.get_greylist_setting(account=mail)
gl_whitelists = iredapd_greylist.get_greylist_whitelists(account=mail)
# Throttling
inbound_throttle_setting = iredapd_throttle.get_throttle_setting(account=mail, inout_type='inbound')
outbound_throttle_setting = iredapd_throttle.get_throttle_setting(account=mail, inout_type='outbound')
# Get managed domains and all domains under control.
managed_domains = []
all_domains = []
if session.get('is_global_admin') or session.get('is_normal_admin') or session.get('allowed_to_grant_admin'):
qr = sql_lib_admin.get_managed_domains(admin=mail,
domain_name_only=True,
listed_only=True,
conn=conn)
if qr[0]:
managed_domains += qr[1]
if session.get('is_global_admin'):
qr = sql_lib_domain.get_all_domains(conn=conn,
columns=['domain', 'description'])
if qr[0]:
all_domains = qr[1]
else:
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.username,
listed_only=True)
if qr[0]:
all_domains = qr[1]
# Get spam policy
spampolicy = {}
global_spam_score = None
if settings.amavisd_enable_policy_lookup:
qr = spampolicylib.get_spam_policy(account=mail)
if not qr[0]:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
else:
spampolicy = qr[1]
global_spam_score = spampolicylib.get_global_spam_score()
# Get per-user white/blacklists
whitelists = []
blacklists = []
outbound_whitelists = []
outbound_blacklists = []
qr = lib_wblist.get_wblist(account=mail)
if qr[0]:
whitelists = qr[1]['inbound_whitelists']
blacklists = qr[1]['inbound_blacklists']
outbound_whitelists = qr[1]['outbound_whitelists']
outbound_blacklists = qr[1]['outbound_blacklists']
return web.render(
'sql/user/profile.html',
cur_domain=domain,
mail=mail,
profile_type=profile_type,
profile=user_profile,
timezones=TIMEZONES,
min_passwd_length=min_passwd_length,
max_passwd_length=max_passwd_length,
store_password_in_plain_text=settings.STORE_PASSWORD_IN_PLAIN_TEXT,
password_policies=iredutils.get_password_policies(),
user_settings=user_settings,
used_quota=used_quota,
last_logins=last_logins,
all_aliases=all_aliases,
assigned_aliases=assigned_aliases,
user_alias_addresses=user_alias_addresses,
user_alias_cross_all_domains=settings.USER_ALIAS_CROSS_ALL_DOMAINS,
all_maillist_profiles=all_maillist_profiles,
all_subscribed_lists=all_subscribed_lists,
disabled_user_profiles=disabled_user_profiles,
allow_nets=allow_nets,
managed_domains=managed_domains,
all_domains=all_domains,
relayhost=relayhost,
# iRedAPD
gl_setting=gl_setting,
gl_whitelists=gl_whitelists,
# iRedAPD
inbound_throttle_setting=inbound_throttle_setting,
outbound_throttle_setting=outbound_throttle_setting,
# spam policy, wblist, throttling
spampolicy=spampolicy,
custom_ban_rules=settings.AMAVISD_BAN_RULES,
global_spam_score=global_spam_score,
whitelists=whitelists,
blacklists=blacklists,
outbound_whitelists=outbound_whitelists,
outbound_blacklists=outbound_blacklists,
languagemaps=iredutils.get_language_maps(),
msg=msg,
discarded_aliases=discarded_aliases,
)
# Don't use decorator `@decorators.require_domain_access` here, because if
# domain admin doesn't manage its own domain, it cannot access its own
# profile.
@decorators.csrf_protected
def POST(self, profile_type, mail):
form = web.input(
enabledService=[],
shadowAddress=[],
telephoneNumber=[],
subscribed_list=[],
memberOfGroup=[],
oldMemberOfAlias=[],
memberOfAlias=[],
domainName=[], # Managed domains
banned_rulenames=[],
)
mail = str(mail).lower()
domain = mail.split('@', 1)[-1]
_wrap = SQLWrap()
conn = _wrap.conn
# - Allow global admin
# - normal admin who manages this domain
# - allow normal admin who doesn't manage this domain, but is updating its own profile
if sql_lib_general.is_domain_admin(domain=domain, admin=session.get('username'), conn=conn) or \
(session.get('is_normal_admin') and session.get('username') == mail):
pass
else:
raise web.seeother('/domains?msg=PERMISSION_DENIED')
result = sql_lib_user.update(conn=conn,
mail=mail,
profile_type=profile_type,
form=form)
if profile_type == 'rename':
profile_type = 'general'
if result[0]:
_discarded_aliases = []
if profile_type == 'aliases':
# Notify admin the discarded addresses.
try:
_discarded_aliases = result[1]['discarded_aliases']
except:
pass
if _discarded_aliases:
raise web.seeother('/profile/user/%s/%s?msg=UPDATED'
'&discarded_aliases=%s' % (profile_type, mail, ','.join(_discarded_aliases)))
else:
raise web.seeother('/profile/user/{}/{}?msg=UPDATED'.format(profile_type, mail))
else:
raise web.seeother('/profile/user/{}/{}?msg={}'.format(profile_type, mail, web.urlquote(result[1])))
class Create:
@decorators.require_domain_access
def GET(self, domain):
domain = str(domain).lower()
form = web.input()
# Get all managed domains.
_wrap = SQLWrap()
conn = _wrap.conn
if session.get('is_global_admin'):
qr = sql_lib_domain.get_all_domains(conn=conn, name_only=True)
else:
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.get('username'),
domain_name_only=True)
if qr[0]:
all_domains = qr[1]
else:
raise web.seeother('/domains?msg=' + web.urlquote(qr[1]))
if not all_domains:
raise web.seeother('/domains?msg=NO_DOMAIN_AVAILABLE')
# Get domain profile.
qr_profile = sql_lib_domain.simple_profile(domain=domain, conn=conn)
if qr_profile[0]:
domain_profile = qr_profile[1]
domain_settings = sqlutils.account_settings_string_to_dict(domain_profile['settings'])
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr_profile[1]))
# Cet total number and allocated quota size of existing users under domain.
num_users_under_domain = sql_lib_general.num_users_under_domain(domain=domain, conn=conn)
used_quota_size = sql_lib_domain.get_allocated_domain_quota(domains=[domain], conn=conn)
db_settings = iredutils.get_settings_from_db()
_min_passwd_length = db_settings['min_passwd_length']
_max_passwd_length = db_settings['max_passwd_length']
min_passwd_length = domain_settings.get('min_passwd_length', _min_passwd_length)
max_passwd_length = domain_settings.get('max_passwd_length', _max_passwd_length)
return web.render(
'sql/user/create.html',
cur_domain=domain,
all_domains=all_domains,
profile=domain_profile,
domain_settings=domain_settings,
min_passwd_length=min_passwd_length,
max_passwd_length=max_passwd_length,
store_password_in_plain_text=settings.STORE_PASSWORD_IN_PLAIN_TEXT,
num_existing_users=num_users_under_domain,
usedQuotaSize=used_quota_size,
languagemaps=iredutils.get_language_maps(),
password_policies=iredutils.get_password_policies(),
msg=form.get('msg'),
)
@decorators.csrf_protected
@decorators.require_domain_access
def POST(self, domain):
domain = str(domain).lower()
form = web.input()
domain_in_form = form_utils.get_domain_name(form)
if domain != domain_in_form:
raise web.seeother('/domains?msg=PERMISSION_DENIED')
# Get domain name, username, cn.
username = form_utils.get_single_value(form,
input_name='username',
to_string=True)
qr = sql_lib_user.add_user_from_form(domain=domain, form=form)
if qr[0]:
raise web.seeother('/profile/user/general/{}@{}?msg=CREATED'.format(username, domain))
else:
raise web.seeother('/create/user/{}?msg={}'.format(domain, web.urlquote(qr[1])))
# Internal domain admins
class Admin:
@decorators.require_domain_access
def GET(self, domain, cur_page=1):
domain = str(domain).lower()
cur_page = int(cur_page) or 1
form = web.input(_unicode=False)
first_char = None
if 'starts_with' in form:
first_char = form.get('starts_with')[:1].upper()
if not iredutils.is_valid_account_first_char(first_char):
first_char = None
_wrap = SQLWrap()
conn = _wrap.conn
_include_global_admins = settings.SHOW_GLOBAL_ADMINS_IN_PER_DOMAIN_ADMIN_LIST
qr = sql_lib_admin.get_paged_domain_admins(conn=conn,
domain=domain,
include_global_admins=_include_global_admins,
current_page=cur_page,
first_char=first_char)
if not qr[0]:
raise web.seeother('/domains?msg=%s' % web.urlquote(qr[1]))
total = qr[1]['total']
records = qr[1]['records']
# Get list of email addresses
mails = []
for r in records:
mails += [str(r.get('username'))]
# Get real-time used quota.
used_quotas = {}
if settings.SHOW_USED_QUOTA:
if mails:
try:
used_quotas = sql_lib_general.get_account_used_quota(accounts=mails, conn=conn)
except Exception:
pass
# Get user forwardings
_status, _result = sql_lib_user.get_bulk_user_forwardings(conn=conn, mails=mails)
if _status:
user_forwardings = _result
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
# Get user alias addresses
(_status, _result) = sql_lib_user.get_bulk_user_alias_addresses(mails=mails, conn=conn)
if _status:
user_alias_addresses = _result
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
# Get assigned groups
(_status, _result) = sql_lib_user.get_bulk_user_assigned_groups(mails=mails, conn=conn)
if _status:
user_assigned_groups = _result
else:
raise web.seeother('/domains?msg=%s' % web.urlquote(_result))
if session.get('is_global_admin'):
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX_FOR_GLOBAL_ADMIN
else:
days_to_keep_removed_mailbox = settings.DAYS_TO_KEEP_REMOVED_MAILBOX
return web.render('sql/user/list.html',
cur_domain=domain,
cur_page=cur_page,
total=total,
users=records,
user_forwardings=user_forwardings,
user_alias_addresses=user_alias_addresses,
user_assigned_groups=user_assigned_groups,
used_quotas=used_quotas,
first_char=first_char,
days_to_keep_removed_mailbox=days_to_keep_removed_mailbox,
all_are_admins=True,
msg=web.input().get('msg', None))
# Preferences allowed to be updated by user
class Preferences:
@decorators.require_user_login
def GET(self, profile_type='general'):
form = web.input()
mail = session['username']
domain = mail.split('@', 1)[-1]
_wrap = SQLWrap()
conn = _wrap.conn
qr = sql_lib_user.profile(mail=mail, conn=conn)
user_profile = qr[1]
del qr
# Get per-user settings
user_settings = {}
qr = sql_lib_general.get_user_settings(conn=conn,
mail=mail,
existing_settings=user_profile['settings'])
if qr[0]:
user_settings = qr[1]
del qr
# Get used quota
used_quota_bytes = 0
if settings.SHOW_USED_QUOTA:
used_quota = sql_lib_general.get_account_used_quota(accounts=[mail], conn=conn)
used_quota_bytes = used_quota.get(mail, {}).get('bytes', 0)
# Get per-domain disabled user preferences.
qr = sql_lib_domain.simple_profile(conn=conn,
domain=domain,
columns=['settings'])
if qr[0]:
domain_profile = qr[1]
domain_settings = sqlutils.account_settings_string_to_dict(domain_profile['settings'])
disabled_user_preferences = domain_settings.get('disabled_user_preferences', [])
session['disabled_user_preferences'] = disabled_user_preferences
db_settings = iredutils.get_settings_from_db()
_min_passwd_length = db_settings['min_passwd_length']
_max_passwd_length = db_settings['max_passwd_length']
min_passwd_length = domain_settings.get('min_passwd_length', _min_passwd_length)
max_passwd_length = domain_settings.get('max_passwd_length', _max_passwd_length)
password_policies = iredutils.get_password_policies()
if min_passwd_length > 0:
password_policies['min_passwd_length'] = min_passwd_length
if max_passwd_length > 0:
password_policies['max_passwd_length'] = max_passwd_length
return web.render(
'sql/self-service/user/preferences.html',
cur_domain=domain,
mail=mail,
profile_type=profile_type,
profile=user_profile,
user_settings=user_settings,
used_quota_bytes=used_quota_bytes,
disabled_user_preferences=disabled_user_preferences,
languagemaps=iredutils.get_language_maps(),
timezones=TIMEZONES,
min_passwd_length=min_passwd_length,
max_passwd_length=max_passwd_length,
store_password_in_plain_text=settings.STORE_PASSWORD_IN_PLAIN_TEXT,
password_policies=password_policies,
msg=form.get('msg'),
)
@decorators.csrf_protected
@decorators.require_user_login
def POST(self, profile_type='general'):
mail = session['username']
form = web.input(telephoneNumber=[])
_wrap = SQLWrap()
conn = _wrap.conn
result = sql_lib_user.update_preferences(conn=conn,
mail=mail,
form=form,
profile_type=profile_type)
if result[0]:
raise web.seeother('/preferences?msg=UPDATED')
else:
raise web.seeother('/preferences?msg=%s' % web.urlquote(result[1]))
# APIProxyUser proxies requests to RESTful API interface without calling
# the exposed `/api/` url.
class APIProxyUser:
@decorators.require_domain_access
def PUT(self, mail):
form = web.input()
qr = sql_lib_user.api_update_profile(mail=mail, form=form, conn=None)
return api_render(qr)
class AllLastLogins:
@decorators.require_domain_access
def GET(self, domain):
domain = domain.lower()
last_logins = sql_lib_general.get_all_last_logins(domain=domain, conn=None)
return web.render(
'sql/user/all_last_logins.html',
cur_domain=domain,
last_logins=last_logins,
# msg=msg,
)

33
controllers/sql/utils.py Normal file
View File

@@ -0,0 +1,33 @@
import web
from controllers.decorators import require_admin_login
from libs.sqllib import SQLWrap
from libs.sqllib import domain as sql_lib_domain
from libs.sqllib import admin as sql_lib_admin
session = web.config.get('_session')
# Get all domains, select the first one.
class CreateDispatcher:
@require_admin_login
def GET(self, account_type):
_wrap = SQLWrap()
conn = _wrap.conn
if session.get('is_global_admin'):
qr = sql_lib_domain.get_all_domains(conn=conn, name_only=True)
else:
qr = sql_lib_admin.get_managed_domains(conn=conn,
admin=session.get('username'),
domain_name_only=True)
if qr[0]:
all_domains = qr[1]
# Go to first available domain.
if all_domains:
raise web.seeother('/create/{}/{}'.format(account_type, all_domains[0]))
else:
raise web.seeother('/domains?msg=NO_DOMAIN_AVAILABLE')
else:
raise web.seeother('/domains?msg=' + web.urlquote(qr[1]))

67
controllers/utils.py Normal file
View File

@@ -0,0 +1,67 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import simplejson as json
import web
class Redirect:
"""Make url ending with or without '/' going to the same class."""
def GET(self, path):
raise web.seeother("/" + str(path))
class Expired:
def GET(self):
web.header("Content-Type", "text/html")
return """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>License expired</title>
</head>
<body>
<p>Your license of iRedAdmin-Pro expired, please <a href="http://www.iredmail.org/pricing.html" target="_blank" rel='noopener'>purchase a new license</a> to continue using iRedAdmin-Pro.</p>
</body>
</html>
"""
def _render_json(d):
web.header("Content-Type", "application/json")
return json.dumps(d)
def api_render(data):
"""Convert given data to a dict and render it.
- if `data` is a dict, return it directly.
- if `data` is a tuple:
- (True, ) -> {'_success': True}
- (True, xxx) -> {'_success': True, '_data': xxx}
- (False, ) -> {'_success': False}
- (False, xxx) -> {'_success': False, '_msg': xxx}
- if `data` is a boolean value (True, False), return {'_success': <boolean>}
"""
if isinstance(data, dict):
d = data
elif isinstance(data, tuple):
if data[0] is True:
if len(data) == 2:
d = {"_success": True, "_data": data[1]}
else:
d = {"_success": True}
else:
if len(data) == 2:
d = {"_success": False, "_msg": data[1]}
else:
d = {"_success": False}
elif isinstance(data, bool):
d = {"_success": data}
else:
d = {"_success": False, "_msg": repr(data)}
return _render_json(d)