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)

15
docs/README.customization Normal file
View File

@@ -0,0 +1,15 @@
* Logo image, favicon.ico, brand name and short description can be defined in
config file (settings.py):
```
BRAND_LOGO = 'logo.png' # load file 'static/logo.png'
BRAND_FAVICON = 'favicon.ico' # load file 'static/favicon.ico'
BRAND_NAME = 'iRedAdmin-Pro'
BRAND_DESC = 'iRedMail Admin Panel'
```
* Link of support page on page footer can be defined in config file (settings.py):
```
URL_SUPPORT = 'http://www.iredmail.org/support.html'
```

7
docs/tests.md Normal file
View File

@@ -0,0 +1,7 @@
# Perform unit tests
## docstring tests
```
python3 -m doctest path/to/file.py
```

View File

@@ -0,0 +1,360 @@
/*
* FancyBox - jQuery Plugin
* Simple and fancy lightbox alternative
*
* Examples and documentation at: http://fancybox.net
*
* Copyright (c) 2008 - 2010 Janis Skarnelis
* That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated.
*
* Version: 1.3.4 (11/11/2010)
* Requires: jQuery v1.3+
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*/
#fancybox-loading {
position: fixed;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin-top: -20px;
margin-left: -20px;
cursor: pointer;
overflow: hidden;
z-index: 1104;
display: none;
}
#fancybox-loading div {
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 480px;
background-image: url('../images/fancybox/fancybox.png');
}
#fancybox-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 1100;
display: none;
}
#fancybox-tmp {
padding: 0;
margin: 0;
border: 0;
overflow: auto;
display: none;
}
#fancybox-wrap {
position: absolute;
top: 0;
left: 0;
padding: 20px;
z-index: 1101;
outline: none;
display: none;
}
#fancybox-outer {
position: relative;
width: 100%;
height: 100%;
background: #fff;
}
#fancybox-content {
width: 0;
height: 0;
padding: 0;
outline: none;
position: relative;
overflow: hidden;
z-index: 1102;
border: 0px solid #fff;
text-align: left;
}
#fancybox-hide-sel-frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
z-index: 1101;
}
#fancybox-close {
position: absolute;
top: -15px;
right: -15px;
width: 30px;
height: 30px;
background: transparent url('../images/fancybox/fancybox.png') -40px 0px;
cursor: pointer;
z-index: 1103;
display: none;
}
#fancybox-error {
color: #444;
font: normal 12px/20px Arial;
padding: 14px;
margin: 0;
}
#fancybox-img {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: none;
outline: none;
line-height: 0;
vertical-align: top;
}
#fancybox-frame {
width: 100%;
height: 100%;
border: none;
display: block;
}
#fancybox-left, #fancybox-right {
position: absolute;
bottom: 0px;
height: 100%;
width: 35%;
cursor: pointer;
outline: none;
background: transparent url('../images/fancybox/fancybox.blank.gif');
z-index: 1102;
display: none;
}
#fancybox-left {
left: 0px;
}
#fancybox-right {
right: 0px;
}
#fancybox-left-ico, #fancybox-right-ico {
position: absolute;
top: 50%;
left: -9999px;
width: 30px;
height: 30px;
margin-top: -15px;
cursor: pointer;
z-index: 1102;
display: block;
}
#fancybox-left-ico {
background-image: url('../images/fancybox/fancybox.png');
background-position: -40px -30px;
}
#fancybox-right-ico {
background-image: url('../images/fancybox/fancybox.png');
background-position: -40px -60px;
}
#fancybox-left:hover, #fancybox-right:hover {
visibility: visible; /* IE6 */
}
#fancybox-left:hover span {
left: 20px;
}
#fancybox-right:hover span {
left: auto;
right: 20px;
}
.fancybox-bg {
position: absolute;
padding: 0;
margin: 0;
border: 0;
width: 20px;
height: 20px;
z-index: 1001;
}
#fancybox-bg-n {
top: -20px;
left: 0;
width: 100%;
background-image: url('../images/fancybox/fancybox-x.png');
}
#fancybox-bg-ne {
top: -20px;
right: -20px;
background-image: url('../images/fancybox/fancybox.png');
background-position: -40px -162px;
}
#fancybox-bg-e {
top: 0;
right: -20px;
height: 100%;
background-image: url('../images/fancybox/fancybox-y.png');
background-position: -20px 0px;
}
#fancybox-bg-se {
bottom: -20px;
right: -20px;
background-image: url('../images/fancybox/fancybox.png');
background-position: -40px -182px;
}
#fancybox-bg-s {
bottom: -20px;
left: 0;
width: 100%;
background-image: url('../images/fancybox/fancybox-x.png');
background-position: 0px -20px;
}
#fancybox-bg-sw {
bottom: -20px;
left: -20px;
background-image: url('../images/fancybox/fancybox.png');
background-position: -40px -142px;
}
#fancybox-bg-w {
top: 0;
left: -20px;
height: 100%;
background-image: url('../images/fancybox/fancybox-y.png');
}
#fancybox-bg-nw {
top: -20px;
left: -20px;
background-image: url('../images/fancybox/fancybox.png');
background-position: -40px -122px;
}
#fancybox-title {
font-family: Helvetica;
font-size: 12px;
z-index: 1102;
}
.fancybox-title-inside {
padding-bottom: 10px;
text-align: center;
color: #333;
background: #fff;
position: relative;
}
.fancybox-title-outside {
padding-top: 10px;
color: #fff;
}
.fancybox-title-over {
position: absolute;
bottom: 0;
left: 0;
color: #FFF;
text-align: left;
}
#fancybox-title-over {
padding: 10px;
background-image: url('fancy_title_over.png');
display: block;
}
.fancybox-title-float {
position: absolute;
left: 0;
bottom: -20px;
height: 32px;
}
#fancybox-title-float-wrap {
border: none;
border-collapse: collapse;
width: auto;
}
#fancybox-title-float-wrap td {
border: none;
white-space: nowrap;
}
#fancybox-title-float-left {
padding: 0 0 0 15px;
background: url('../images/fancybox/fancybox.png') -40px -90px no-repeat;
}
#fancybox-title-float-main {
color: #FFF;
line-height: 29px;
font-weight: bold;
padding: 0 0 3px 0;
background: url('../images/fancybox/fancybox-x.png') 0px -40px;
}
#fancybox-title-float-right {
padding: 0 0 0 15px;
background: url('../images/fancybox/fancybox.png') -55px -90px no-repeat;
}
/* IE6 */
.fancybox-ie6 #fancybox-close { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_close.png', sizingMethod='scale'); }
.fancybox-ie6 #fancybox-left-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_left.png', sizingMethod='scale'); }
.fancybox-ie6 #fancybox-right-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_right.png', sizingMethod='scale'); }
.fancybox-ie6 #fancybox-title-over { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_over.png', sizingMethod='scale'); zoom: 1; }
.fancybox-ie6 #fancybox-title-float-left { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_left.png', sizingMethod='scale'); }
.fancybox-ie6 #fancybox-title-float-main { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_main.png', sizingMethod='scale'); }
.fancybox-ie6 #fancybox-title-float-right { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_right.png', sizingMethod='scale'); }
.fancybox-ie6 #fancybox-bg-w, .fancybox-ie6 #fancybox-bg-e, .fancybox-ie6 #fancybox-left, .fancybox-ie6 #fancybox-right, #fancybox-hide-sel-frame {
height: expression(this.parentNode.clientHeight + "px");
}
#fancybox-loading.fancybox-ie6 {
position: absolute; margin-top: 0;
top: expression( (-20 + (document.documentElement.clientHeight ? document.documentElement.clientHeight/2 : document.body.clientHeight/2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )) + 'px');
}
#fancybox-loading.fancybox-ie6 div { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_loading.png', sizingMethod='scale'); }
/* IE6, IE7, IE8 */
.fancybox-ie .fancybox-bg { background: transparent !important; }
.fancybox-ie #fancybox-bg-n { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_n.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-ne { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_ne.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-e { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_e.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-se { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_se.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-s { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_s.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-sw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_sw.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-w { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_w.png', sizingMethod='scale'); }
.fancybox-ie #fancybox-bg-nw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_nw.png', sizingMethod='scale'); }

View File

@@ -0,0 +1,53 @@
/* Eric Meyer's CSS Reset v1.0 | 20080212 */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 13px;
vertical-align: baseline;
background: transparent;
}
table, tbody, tfoot, thead, tr, th, td {
vertical-align: top;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
/* remember to define focus styles! */
:focus {
outline: 0;
}
/* remember to highlight inserts somehow! */
ins {
text-decoration: none;
}
del {
text-decoration: line-through;
}
/* tables still need 'cellspacing="0"' in the markup */
table {
border-collapse: collapse;
border-spacing: 0;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1
static/default/css/spectre.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,803 @@
<?xml version="1.0" standalone="no"?>
<!--
Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020
By Robert Madole
Copyright (c) Font Awesome
</metadata>
<defs>
<font id="FontAwesome5Free-Regular" horiz-adv-x="512" >
<font-face
font-family="Font Awesome 5 Free Regular"
font-weight="400"
font-stretch="normal"
units-per-em="512"
panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="448"
descent="-64"
bbox="-0.0663408 -64.0662 640.01 448.1"
underline-thickness="25"
underline-position="-50"
unicode-range="U+0020-F5C8"
/>
<missing-glyph />
<glyph glyph-name="heart" unicode="&#xf004;"
d="M458.4 383.7c75.2998 -63.4004 64.0996 -166.601 10.5996 -221.3l-175.4 -178.7c-10 -10.2002 -23.2998 -15.7998 -37.5996 -15.7998c-14.2002 0 -27.5996 5.69922 -37.5996 15.8994l-175.4 178.7c-53.5996 54.7002 -64.5996 157.9 10.5996 221.2
c57.8008 48.7002 147.101 41.2998 202.4 -15c55.2998 56.2998 144.6 63.5996 202.4 15zM434.8 196.2c36.2002 36.8994 43.7998 107.7 -7.2998 150.8c-38.7002 32.5996 -98.7002 27.9004 -136.5 -10.5996l-35 -35.7002l-35 35.7002
c-37.5996 38.2998 -97.5996 43.1992 -136.5 10.5c-51.2002 -43.1006 -43.7998 -113.5 -7.2998 -150.7l175.399 -178.7c2.40039 -2.40039 4.40039 -2.40039 6.80078 0z" />
<glyph glyph-name="star" unicode="&#xf005;" horiz-adv-x="576"
d="M528.1 276.5c26.2002 -3.7998 36.7002 -36.0996 17.7002 -54.5996l-105.7 -103l25 -145.5c4.5 -26.3008 -23.1992 -45.9004 -46.3994 -33.7002l-130.7 68.7002l-130.7 -68.7002c-23.2002 -12.2998 -50.8994 7.39941 -46.3994 33.7002l25 145.5l-105.7 103
c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2998l65.2998 132.4c11.7998 23.8994 45.7002 23.5996 57.4004 0l65.2998 -132.4zM388.6 135.7l100.601 98l-139 20.2002l-62.2002 126l-62.2002 -126l-139 -20.2002l100.601 -98l-23.7002 -138.4l124.3 65.2998
l124.3 -65.2998z" />
<glyph glyph-name="user" unicode="&#xf007;" horiz-adv-x="448"
d="M313.6 144c74.2002 0 134.4 -60.2002 134.4 -134.4v-25.5996c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v25.5996c0 74.2002 60.2002 134.4 134.4 134.4c28.7998 0 42.5 -16 89.5996 -16s60.9004 16 89.5996 16zM400 -16v25.5996
c0 47.6006 -38.7998 86.4004 -86.4004 86.4004c-14.6992 0 -37.8994 -16 -89.5996 -16c-51.2998 0 -75 16 -89.5996 16c-47.6006 0 -86.4004 -38.7998 -86.4004 -86.4004v-25.5996h352zM224 160c-79.5 0 -144 64.5 -144 144s64.5 144 144 144s144 -64.5 144 -144
s-64.5 -144 -144 -144zM224 400c-52.9004 0 -96 -43.0996 -96 -96s43.0996 -96 96 -96s96 43.0996 96 96s-43.0996 96 -96 96z" />
<glyph glyph-name="clock" unicode="&#xf017;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM317.8 96.4004l-84.8994 61.6992
c-3.10059 2.30078 -4.90039 5.90039 -4.90039 9.7002v164.2c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12v-141.7l66.7998 -48.5996c5.40039 -3.90039 6.5 -11.4004 2.60059 -16.7998l-18.8008 -25.9004c-3.89941 -5.2998 -11.3994 -6.5 -16.7998 -2.59961z
" />
<glyph glyph-name="list-alt" unicode="&#xf022;"
d="M464 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h416zM458 16c3.31152 0 6 2.68848 6 6v340c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-340
c0 -3.31152 2.68848 -6 6 -6h404zM416 108v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 204v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12
v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 300v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM164 288c0 -19.8818 -16.1182 -36 -36 -36
s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 192c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 96c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36z" />
<glyph glyph-name="flag" unicode="&#xf024;"
d="M336.174 368c35.4668 0 73.0195 12.6914 108.922 28.1797c31.6406 13.6514 66.9043 -9.65723 66.9043 -44.1162v-239.919c0 -16.1953 -8.1543 -31.3057 -21.7129 -40.1631c-26.5762 -17.3643 -70.0693 -39.9814 -128.548 -39.9814c-68.6084 0 -112.781 32 -161.913 32
c-56.5674 0 -89.957 -11.2803 -127.826 -28.5566v-83.4434c0 -8.83691 -7.16309 -16 -16 -16h-16c-8.83691 0 -16 7.16309 -16 16v406.438c-14.3428 8.2998 -24 23.7979 -24 41.5615c0 27.5693 23.2422 49.71 51.2012 47.8965
c22.9658 -1.49023 41.8662 -19.4717 44.4805 -42.3379c0.177734 -1.52441 0.321289 -4.00781 0.321289 -5.54199c0 -4.30176 -1.10352 -11.1035 -2.46289 -15.1846c22.418 8.68555 49.4199 15.168 80.7207 15.168c68.6084 0 112.781 -32 161.913 -32zM464 112v240
c-31.5059 -14.6338 -84.5547 -32 -127.826 -32c-59.9111 0 -101.968 32 -161.913 32c-41.4365 0 -80.4766 -16.5879 -102.261 -32v-232c31.4473 14.5967 84.4648 24 127.826 24c59.9111 0 101.968 -32 161.913 -32c41.4365 0 80.4775 16.5879 102.261 32z" />
<glyph glyph-name="bookmark" unicode="&#xf02e;" horiz-adv-x="384"
d="M336 448c26.5098 0 48 -21.4902 48 -48v-464l-192 112l-192 -112v464c0 26.5098 21.4902 48 48 48h288zM336 19.5703v374.434c0 3.31348 -2.68555 5.99609 -6 5.99609h-276c-3.31152 0 -6 -2.68848 -6 -6v-374.43l144 84z" />
<glyph glyph-name="image" unicode="&#xf03e;"
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM458 48c3.31152 0 6 2.68848 6 6v276c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-276
c0 -3.31152 2.68848 -6 6 -6h404zM128 296c22.0908 0 40 -17.9092 40 -40s-17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40zM96 96v48l39.5137 39.5146c4.6875 4.68652 12.2852 4.68652 16.9717 0l39.5146 -39.5146l119.514 119.515
c4.6875 4.68652 12.2852 4.68652 16.9717 0l87.5146 -87.5146v-80h-320z" />
<glyph glyph-name="edit" unicode="&#xf044;" horiz-adv-x="576"
d="M402.3 103.1l32 32c5 5 13.7002 1.5 13.7002 -5.69922v-145.4c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h273.5c7.09961 0 10.7002 -8.59961 5.7002 -13.7002l-32 -32c-1.5 -1.5 -3.5 -2.2998 -5.7002 -2.2998h-241.5v-352h352
v113.5c0 2.09961 0.799805 4.09961 2.2998 5.59961zM558.9 304.9l-262.601 -262.601l-90.3994 -10c-26.2002 -2.89941 -48.5 19.2002 -45.6006 45.6006l10 90.3994l262.601 262.601c22.8994 22.8994 59.8994 22.8994 82.6992 0l43.2002 -43.2002
c22.9004 -22.9004 22.9004 -60 0.100586 -82.7998zM460.1 274l-58.0996 58.0996l-185.8 -185.899l-7.2998 -65.2998l65.2998 7.2998zM524.9 353.7l-43.2002 43.2002c-4.10059 4.09961 -10.7998 4.09961 -14.7998 0l-30.9004 -30.9004l58.0996 -58.0996l30.9004 30.8994
c4 4.2002 4 10.7998 -0.0996094 14.9004z" />
<glyph glyph-name="times-circle" unicode="&#xf057;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM357.8 254.2l-62.2002 -62.2002l62.2002 -62.2002
c4.7002 -4.7002 4.7002 -12.2998 0 -17l-22.5996 -22.5996c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-62.2002 62.2002l-62.2002 -62.2002c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-22.5996 22.5996c-4.7002 4.7002 -4.7002 12.2998 0 17l62.2002 62.2002l-62.2002 62.2002
c-4.7002 4.7002 -4.7002 12.2998 0 17l22.5996 22.5996c4.7002 4.7002 12.2998 4.7002 17 0l62.2002 -62.2002l62.2002 62.2002c4.7002 4.7002 12.2998 4.7002 17 0l22.5996 -22.5996c4.7002 -4.7002 4.7002 -12.2998 0 -17z" />
<glyph glyph-name="check-circle" unicode="&#xf058;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200z
M396.204 261.733c4.66699 -4.70508 4.63672 -12.3037 -0.0673828 -16.9717l-172.589 -171.204c-4.70508 -4.66797 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66797 4.70605 -4.63672 12.3047 0.0683594 16.9717l22.7188 22.5361
c4.70508 4.66699 12.3027 4.63574 16.9697 -0.0693359l59.792 -60.2773l141.353 140.216c4.70508 4.66797 12.3027 4.6377 16.9697 -0.0673828z" />
<glyph glyph-name="question-circle" unicode="&#xf059;"
d="M256 440c136.957 0 248 -111.083 248 -248c0 -136.997 -111.043 -248 -248 -248s-248 111.003 -248 248c0 136.917 111.043 248 248 248zM256 -8c110.569 0 200 89.4697 200 200c0 110.529 -89.5088 200 -200 200c-110.528 0 -200 -89.5049 -200 -200
c0 -110.569 89.4678 -200 200 -200zM363.244 247.2c0 -67.0518 -72.4209 -68.084 -72.4209 -92.8633v-6.33691c0 -6.62695 -5.37305 -12 -12 -12h-45.6475c-6.62695 0 -12 5.37305 -12 12v8.65918c0 35.7451 27.1006 50.0342 47.5791 61.5156
c17.5615 9.84473 28.3242 16.541 28.3242 29.5791c0 17.2461 -21.999 28.6934 -39.7842 28.6934c-23.1885 0 -33.8936 -10.9775 -48.9424 -29.9697c-4.05664 -5.11914 -11.46 -6.07031 -16.666 -2.12402l-27.8232 21.0986
c-5.10742 3.87207 -6.25098 11.0654 -2.64453 16.3633c23.627 34.6934 53.7217 54.1846 100.575 54.1846c49.0713 0 101.45 -38.3037 101.45 -88.7998zM298 80c0 -23.1592 -18.8408 -42 -42 -42s-42 18.8408 -42 42s18.8408 42 42 42s42 -18.8408 42 -42z" />
<glyph glyph-name="eye" unicode="&#xf06e;" horiz-adv-x="576"
d="M288 304c0.0927734 0 0.244141 0.000976562 0.336914 0.000976562c61.6641 0 111.71 -50.0469 111.71 -111.711c0 -61.6631 -50.0459 -111.71 -111.71 -111.71s-111.71 50.0469 -111.71 111.71c0 8.71289 1.95898 22.5781 4.37305 30.9502
c6.93066 -3.94141 19.0273 -7.18457 27 -7.24023c30.9121 0 56 25.0879 56 56c-0.0556641 7.97266 -3.29883 20.0693 -7.24023 27c8.42383 2.62207 22.4189 4.8623 31.2402 5zM572.52 206.6c1.9209 -3.79883 3.47949 -10.3379 3.47949 -14.5947
s-1.55859 -10.7959 -3.47949 -14.5947c-54.1992 -105.771 -161.59 -177.41 -284.52 -177.41s-230.29 71.5898 -284.52 177.4c-1.9209 3.79883 -3.47949 10.3379 -3.47949 14.5947s1.55859 10.7959 3.47949 14.5947c54.1992 105.771 161.59 177.41 284.52 177.41
s230.29 -71.5898 284.52 -177.4zM288 48c98.6602 0 189.1 55 237.93 144c-48.8398 89 -139.27 144 -237.93 144s-189.09 -55 -237.93 -144c48.8398 -89 139.279 -144 237.93 -144z" />
<glyph glyph-name="eye-slash" unicode="&#xf070;" horiz-adv-x="640"
d="M634 -23c3.31738 -2.65137 6.00977 -8.25098 6.00977 -12.498c0 -3.10449 -1.57715 -7.58984 -3.51953 -10.0117l-10 -12.4902c-2.65234 -3.31152 -8.24707 -6 -12.4902 -6c-3.09961 0 -7.58008 1.57227 -10 3.50977l-598 467.49
c-3.31738 2.65137 -6.00977 8.25098 -6.00977 12.498c0 3.10449 1.57715 7.58984 3.51953 10.0117l10 12.4902c2.65234 3.31152 8.24707 6 12.4902 6c3.09961 0 7.58008 -1.57227 10 -3.50977zM296.79 301.53c6.33496 1.35059 16.7324 2.45801 23.21 2.46973
c60.4805 0 109.36 -47.9102 111.58 -107.85zM343.21 82.46c-6.33496 -1.34375 -16.7334 -2.44629 -23.21 -2.45996c-60.4697 0 -109.35 47.9102 -111.58 107.84zM320 336c-19.8799 0 -39.2803 -2.7998 -58.2197 -7.09961l-46.4102 36.29
c32.9199 11.8096 67.9297 18.8096 104.63 18.8096c122.93 0 230.29 -71.5898 284.57 -177.4c1.91992 -3.79883 3.47949 -10.3379 3.47949 -14.5947s-1.55957 -10.7959 -3.47949 -14.5947c-11.7197 -22.7598 -35.4189 -56.4092 -52.9004 -75.1104l-37.7402 29.5
c14.333 15.0156 34.0449 41.9854 44 60.2002c-48.8398 89 -139.279 144 -237.93 144zM320 48c19.8896 0 39.2803 2.7998 58.2197 7.08984l46.4102 -36.2803c-32.9199 -11.7598 -67.9297 -18.8096 -104.63 -18.8096c-122.92 0 -230.28 71.5898 -284.51 177.4
c-1.9209 3.79883 -3.47949 10.3379 -3.47949 14.5947s1.55859 10.7959 3.47949 14.5947c11.7168 22.7568 35.4111 56.4014 52.8896 75.1006l37.7402 -29.5c-14.3467 -15.0107 -34.0811 -41.9756 -44.0498 -60.1904c48.8496 -89 139.279 -144 237.93 -144z" />
<glyph glyph-name="calendar-alt" unicode="&#xf073;" horiz-adv-x="448"
d="M148 160h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM256 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-40zM352 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM256 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM160 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM352 76c0 -6.59961 -5.40039 -12 -12 -12h-40
c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="comment" unicode="&#xf075;"
d="M256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5c-3.80078 8.7998 -2 19 4.59961 26
c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004
l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
<glyph glyph-name="folder" unicode="&#xf07b;"
d="M464 320c26.5098 0 48 -21.4902 48 -48v-224c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h146.74c8.49023 0 16.6299 -3.37012 22.6299 -9.37012l54.6299 -54.6299h192zM464 48v224h-198.62
c-8.49023 0 -16.6299 3.37012 -22.6299 9.37012l-54.6299 54.6299h-140.12v-288h416z" />
<glyph glyph-name="folder-open" unicode="&#xf07c;" horiz-adv-x="576"
d="M527.9 224c37.6992 0 60.6992 -41.5 40.6992 -73.4004l-79.8994 -128c-8.7998 -14.0996 -24.2002 -22.5996 -40.7002 -22.5996h-400c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h160l64 -64h160c26.5 0 48 -21.5 48 -48v-48h47.9004zM48 330v-233.4l62.9004 104.2
c8.69922 14.4004 24.2998 23.2002 41.0996 23.2002h280v42c0 3.2998 -2.7002 6 -6 6h-173.9l-64 64h-134.1c-3.2998 0 -6 -2.7002 -6 -6zM448 48l80 128h-378.8l-77.2002 -128h376z" />
<glyph glyph-name="chart-bar" unicode="&#xf080;"
d="M396.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v230.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-230.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM204.8 96
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v198.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-198.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM300.8 96
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v134.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-134.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM496 48c8.83984 0 16 -7.16016 16 -16v-16
c0 -8.83984 -7.16016 -16 -16 -16h-464c-17.6699 0 -32 14.3301 -32 32v336c0 8.83984 7.16016 16 16 16h16c8.83984 0 16 -7.16016 16 -16v-320h448zM108.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v70.4004c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004
c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-70.4004c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004z" />
<glyph glyph-name="comments" unicode="&#xf086;" horiz-adv-x="576"
d="M532 61.7998c15.2998 -30.7002 37.4004 -54.5 37.7998 -54.7998c6.2998 -6.7002 8 -16.5 4.40039 -25c-3.7002 -8.5 -12 -14 -21.2002 -14c-53.5996 0 -96.7002 20.2998 -125.2 38.7998c-19 -4.39941 -39 -6.7998 -59.7998 -6.7998
c-86.2002 0 -159.9 40.4004 -191.3 97.7998c-9.7002 1.2002 -19.2002 2.7998 -28.4004 4.90039c-28.5 -18.6006 -71.7002 -38.7998 -125.2 -38.7998c-9.19922 0 -17.5996 5.5 -21.1992 14c-3.7002 8.5 -1.90039 18.2998 4.39941 25
c0.400391 0.399414 22.4004 24.1992 37.7002 54.8994c-27.5 27.2002 -44 61.2002 -44 98.2002c0 88.4004 93.0996 160 208 160c86.2998 0 160.3 -40.5 191.8 -98.0996c99.7002 -11.8008 176.2 -77.9004 176.2 -157.9c0 -37.0996 -16.5 -71.0996 -44 -98.2002zM139.2 154.1
l19.7998 -4.5c16 -3.69922 32.5 -5.59961 49 -5.59961c86.7002 0 160 51.2998 160 112s-73.2998 112 -160 112s-160 -51.2998 -160 -112c0 -28.7002 16.2002 -50.5996 29.7002 -64l24.7998 -24.5l-15.5 -31.0996c-2.59961 -5.10059 -5.2998 -10.1006 -8 -14.8008
c14.5996 5.10059 29 12.3008 43.0996 21.4004zM498.3 96c13.5 13.4004 29.7002 35.2998 29.7002 64c0 49.2002 -48.2998 91.5 -112.7 106c0.299805 -3.2998 0.700195 -6.59961 0.700195 -10c0 -80.9004 -78 -147.5 -179.3 -158.3
c29.0996 -29.6006 77.2998 -49.7002 131.3 -49.7002c16.5 0 33 1.90039 49 5.59961l19.9004 4.60059l17.0996 -11.1006c14.0996 -9.09961 28.5 -16.2998 43.0996 -21.3994c-2.69922 4.7002 -5.39941 9.7002 -8 14.7998l-15.5 31.0996z" />
<glyph glyph-name="star-half" unicode="&#xf089;" horiz-adv-x="576"
d="M288 62.7002v-54.2998l-130.7 -68.6006c-23.3994 -12.2998 -50.8994 7.60059 -46.3994 33.7002l25 145.5l-105.7 103c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2002l65.2998 132.4c5.90039 11.8994 17.2998 17.7998 28.7002 17.7998v-68.0996l-62.2002 -126
l-139 -20.2002l100.601 -98l-23.7002 -138.4z" />
<glyph glyph-name="lemon" unicode="&#xf094;"
d="M484.112 420.111c28.1221 -28.123 35.9434 -68.0039 19.0215 -97.0547c-23.0576 -39.584 50.1436 -163.384 -82.3311 -295.86c-132.301 -132.298 -256.435 -59.3594 -295.857 -82.3291c-29.0459 -16.917 -68.9219 -9.11426 -97.0576 19.0205
c-28.1221 28.1221 -35.9434 68.0029 -19.0215 97.0547c23.0566 39.5859 -50.1436 163.386 82.3301 295.86c132.308 132.309 256.407 59.3496 295.862 82.332c29.0498 16.9219 68.9307 9.09863 97.0537 -19.0234zM461.707 347.217
c13.5166 23.2031 -27.7578 63.7314 -50.4883 50.4912c-66.6025 -38.7939 -165.646 45.5898 -286.081 -74.8457c-120.444 -120.445 -36.0449 -219.472 -74.8447 -286.08c-13.542 -23.2471 27.8145 -63.6953 50.4932 -50.4883
c66.6006 38.7949 165.636 -45.5996 286.076 74.8428c120.444 120.445 36.0449 219.472 74.8447 286.08zM291.846 338.481c1.37012 -10.96 -6.40332 -20.957 -17.3643 -22.3271c-54.8467 -6.85547 -135.779 -87.7871 -142.636 -142.636
c-1.37305 -10.9883 -11.3984 -18.7334 -22.3262 -17.3643c-10.9609 1.37012 -18.7344 11.3652 -17.3643 22.3262c9.16211 73.2852 104.167 168.215 177.364 177.364c10.9531 1.36816 20.9561 -6.40234 22.3262 -17.3633z" />
<glyph glyph-name="credit-card" unicode="&#xf09d;" horiz-adv-x="576"
d="M527.9 416c26.5996 0 48.0996 -21.5 48.0996 -48v-352c0 -26.5 -21.5 -48 -48.0996 -48h-479.801c-26.5996 0 -48.0996 21.5 -48.0996 48v352c0 26.5 21.5 48 48.0996 48h479.801zM54.0996 368c-3.2998 0 -6 -2.7002 -6 -6v-42h479.801v42c0 3.2998 -2.7002 6 -6 6
h-467.801zM521.9 16c3.2998 0 6 2.7002 6 6v170h-479.801v-170c0 -3.2998 2.7002 -6 6 -6h467.801zM192 116v-40c0 -6.59961 -5.40039 -12 -12 -12h-72c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h72c6.59961 0 12 -5.40039 12 -12zM384 116v-40
c0 -6.59961 -5.40039 -12 -12 -12h-136c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h136c6.59961 0 12 -5.40039 12 -12z" />
<glyph glyph-name="hdd" unicode="&#xf0a0;" horiz-adv-x="576"
d="M567.403 212.358c5.59668 -8.04688 8.59668 -17.6113 8.59668 -27.4121v-136.946c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v136.946c0 8.30957 3.85156 20.5898 8.59668 27.4121l105.08 151.053
c7.90625 11.3652 25.5596 20.5889 39.4033 20.5889h0.000976562h269.838h0.000976562c13.8438 0 31.4971 -9.22363 39.4033 -20.5889zM153.081 336l-77.9131 -112h425.664l-77.9131 112h-269.838zM528 48v128h-480v-128h480zM496 112c0 -17.6729 -14.3271 -32 -32 -32
s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32zM400 112c0 -17.6729 -14.3271 -32 -32 -32s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32z" />
<glyph glyph-name="hand-point-right" unicode="&#xf0a4;"
d="M428.8 310.4c45.0996 0 83.2002 -38.1016 83.2002 -83.2002c0 -45.6162 -37.7646 -83.2002 -83.2002 -83.2002h-35.6475c-1.41602 -6.36719 -4.96875 -16.252 -7.92969 -22.0645c2.50586 -22.0059 -3.50293 -44.9775 -15.9844 -62.791
c-1.14062 -52.4863 -37.3984 -91.1445 -99.9404 -91.1445h-21.2988c-60.0635 0 -98.5117 40 -127.2 40h-2.67871c-5.74707 -4.95215 -13.5361 -8 -22.1201 -8h-64c-17.6729 0 -32 12.8936 -32 28.7998v230.4c0 15.9062 14.3271 28.7998 32 28.7998h64.001
c8.58398 0 16.373 -3.04785 22.1201 -8h2.67871c6.96387 0 14.8623 6.19336 30.1816 23.6689l0.128906 0.148438l0.130859 0.145508c8.85645 9.93652 18.1162 20.8398 25.8506 33.2529c18.7051 30.2471 30.3936 78.7842 75.707 78.7842c56.9277 0 92 -35.2861 92 -83.2002
v-0.0839844c0 -6.21777 -0.974609 -16.2148 -2.17578 -22.3154h86.1768zM428.8 192c18.9756 0 35.2002 16.2246 35.2002 35.2002c0 18.7002 -16.7754 35.2002 -35.2002 35.2002h-158.399c0 17.3242 26.3994 35.1992 26.3994 70.3994c0 26.4004 -20.625 35.2002 -44 35.2002
c-8.79395 0 -20.4443 -32.7119 -34.9258 -56.0996c-9.07422 -14.5752 -19.5244 -27.2256 -30.7988 -39.875c-16.1094 -18.374 -33.8359 -36.6328 -59.0752 -39.5967v-176.753c42.79 -3.7627 74.5088 -39.6758 120 -39.6758h21.2988
c40.5244 0 57.124 22.1973 50.6006 61.3252c14.6113 8.00098 24.1514 33.9785 12.9248 53.625c19.3652 18.2246 17.7871 46.3809 4.9502 61.0498h91.0254zM88 64c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
<glyph glyph-name="hand-point-left" unicode="&#xf0a5;"
d="M0 227.2c0 45.0986 38.1006 83.2002 83.2002 83.2002h86.1758c-1.3623 6.91016 -2.17578 14.374 -2.17578 22.3994c0 47.9141 35.0723 83.2002 92 83.2002c45.3135 0 57.002 -48.5371 75.7061 -78.7852c7.73438 -12.4121 16.9951 -23.3154 25.8506 -33.2529
l0.130859 -0.145508l0.128906 -0.148438c15.3213 -17.4746 23.2197 -23.668 30.1836 -23.668h2.67871c5.74707 4.95215 13.5361 8 22.1201 8h64c17.6729 0 32 -12.8936 32 -28.7998v-230.4c0 -15.9062 -14.3271 -28.7998 -32 -28.7998h-64
c-8.58398 0 -16.373 3.04785 -22.1201 8h-2.67871c-28.6885 0 -67.1367 -40 -127.2 -40h-21.2988c-62.542 0 -98.8008 38.6582 -99.9404 91.1445c-12.4814 17.8135 -18.4922 40.7852 -15.9844 62.791c-2.96094 5.8125 -6.51367 15.6973 -7.92969 22.0645h-35.6465
c-45.4355 0 -83.2002 37.584 -83.2002 83.2002zM48 227.2c0 -18.9756 16.2246 -35.2002 35.2002 -35.2002h91.0244c-12.8369 -14.6689 -14.415 -42.8252 4.9502 -61.0498c-11.2256 -19.6465 -1.68652 -45.624 12.9248 -53.625
c-6.52246 -39.1279 10.0771 -61.3252 50.6016 -61.3252h21.2988c45.4912 0 77.21 35.9131 120 39.6768v176.752c-25.2393 2.96289 -42.9658 21.2227 -59.0752 39.5967c-11.2744 12.6494 -21.7246 25.2998 -30.7988 39.875
c-14.4814 23.3877 -26.1318 56.0996 -34.9258 56.0996c-23.375 0 -44 -8.7998 -44 -35.2002c0 -35.2002 26.3994 -53.0752 26.3994 -70.3994h-158.399c-18.4248 0 -35.2002 -16.5 -35.2002 -35.2002zM448 88c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24
s24 10.7451 24 24s-10.7451 24 -24 24z" />
<glyph glyph-name="hand-point-up" unicode="&#xf0a6;" horiz-adv-x="448"
d="M105.6 364.8c0 45.0996 38.1016 83.2002 83.2002 83.2002c45.6162 0 83.2002 -37.7646 83.2002 -83.2002v-35.6465c6.36719 -1.41602 16.252 -4.96875 22.0645 -7.92969c22.0059 2.50684 44.9775 -3.50293 62.791 -15.9844
c52.4863 -1.14062 91.1445 -37.3984 91.1445 -99.9404v-21.2988c0 -60.0635 -40 -98.5117 -40 -127.2v-2.67871c4.95215 -5.74707 8 -13.5361 8 -22.1201v-64c0 -17.6729 -12.8936 -32 -28.7998 -32h-230.4c-15.9062 0 -28.7998 14.3271 -28.7998 32v64
c0 8.58398 3.04785 16.373 8 22.1201v2.67871c0 6.96387 -6.19336 14.8623 -23.6689 30.1816l-0.148438 0.128906l-0.145508 0.130859c-9.93652 8.85645 -20.8398 18.1162 -33.2529 25.8506c-30.2471 18.7051 -78.7842 30.3936 -78.7842 75.707
c0 56.9277 35.2861 92 83.2002 92h0.0839844c6.21777 0 16.2148 -0.974609 22.3154 -2.17578v86.1768zM224 364.8c0 18.9756 -16.2246 35.2002 -35.2002 35.2002c-18.7002 0 -35.2002 -16.7754 -35.2002 -35.2002v-158.399c-17.3242 0 -35.1992 26.3994 -70.3994 26.3994
c-26.4004 0 -35.2002 -20.625 -35.2002 -44c0 -8.79395 32.7119 -20.4443 56.0996 -34.9258c14.5752 -9.07422 27.2256 -19.5244 39.875 -30.7988c18.374 -16.1094 36.6328 -33.8359 39.5967 -59.0752h176.753c3.7627 42.79 39.6758 74.5088 39.6758 120v21.2988
c0 40.5244 -22.1973 57.124 -61.3252 50.6006c-8.00098 14.6113 -33.9785 24.1514 -53.625 12.9248c-18.2246 19.3652 -46.3809 17.7871 -61.0498 4.9502v91.0254zM352 24c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24z" />
<glyph glyph-name="hand-point-down" unicode="&#xf0a7;" horiz-adv-x="448"
d="M188.8 -64c-45.0986 0 -83.2002 38.1006 -83.2002 83.2002v86.1758c-6.91016 -1.3623 -14.374 -2.17578 -22.3994 -2.17578c-47.9141 0 -83.2002 35.0723 -83.2002 92c0 45.3135 48.5371 57.002 78.7852 75.707c12.4121 7.73438 23.3154 16.9951 33.2529 25.8506
l0.145508 0.130859l0.148438 0.128906c17.4746 15.3213 23.668 23.2197 23.668 30.1836v2.67871c-4.95215 5.74707 -8 13.5361 -8 22.1201v64c0 17.6729 12.8936 32 28.7998 32h230.4c15.9062 0 28.7998 -14.3271 28.7998 -32v-64.001
c0 -8.58398 -3.04785 -16.373 -8 -22.1201v-2.67871c0 -28.6885 40 -67.1367 40 -127.2v-21.2988c0 -62.542 -38.6582 -98.8008 -91.1445 -99.9404c-17.8135 -12.4814 -40.7852 -18.4922 -62.791 -15.9844c-5.8125 -2.96094 -15.6973 -6.51367 -22.0645 -7.92969v-35.6465
c0 -45.4355 -37.584 -83.2002 -83.2002 -83.2002zM188.8 -16c18.9756 0 35.2002 16.2246 35.2002 35.2002v91.0244c14.6689 -12.8369 42.8252 -14.415 61.0498 4.9502c19.6465 -11.2256 45.624 -1.68652 53.625 12.9248c39.1279 -6.52246 61.3252 10.0771 61.3252 50.6016
v21.2988c0 45.4912 -35.9131 77.21 -39.6768 120h-176.752c-2.96289 -25.2393 -21.2227 -42.9658 -39.5967 -59.0752c-12.6494 -11.2744 -25.2998 -21.7246 -39.875 -30.7988c-23.3877 -14.4814 -56.0996 -26.1318 -56.0996 -34.9258c0 -23.375 8.7998 -44 35.2002 -44
c35.2002 0 53.0752 26.3994 70.3994 26.3994v-158.399c0 -18.4248 16.5 -35.2002 35.2002 -35.2002zM328 384c0 -13.2549 10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24z" />
<glyph glyph-name="copy" unicode="&#xf0c5;" horiz-adv-x="448"
d="M433.941 382.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-80v-48c0 -26.5098 -21.4902 -48 -48 -48h-224c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h80v48c0 26.5098 21.4902 48 48 48
h172.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM266 -16c3.31152 0 6 2.68848 6 6v42h-96c-26.5098 0 -48 21.4902 -48 48v224h-74c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM394 80c3.31152 0 6 2.68848 6 6v202h-88
c-13.2549 0 -24 10.7451 -24 24v88h-106c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM400 336v9.63184v0.000976562c0 1.37207 -0.787109 3.27246 -1.75684 4.24219l-48.3682 48.3682c-1.12598 1.125 -2.65234 1.75684 -4.24316 1.75684h-9.63184
v-64h64z" />
<glyph glyph-name="save" unicode="&#xf0c7;" horiz-adv-x="448"
d="M433.941 318.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h268.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM272 368h-128v-80h128v80
zM394 16c3.31152 0 6 2.68848 6 6v259.632v0.000976562c0 1.37207 -0.787109 3.27246 -1.75684 4.24219l-78.2432 78.2432v-100.118c0 -13.2549 -10.7451 -24 -24 -24h-176c-13.2549 0 -24 10.7451 -24 24v104h-42c-3.31152 0 -6 -2.68848 -6 -6v-340
c0 -3.31152 2.68848 -6 6 -6h340zM224 216c48.5234 0 88 -39.4766 88 -88s-39.4766 -88 -88 -88s-88 39.4766 -88 88s39.4766 88 88 88zM224 88c22.0557 0 40 17.9443 40 40s-17.9443 40 -40 40s-40 -17.9443 -40 -40s17.9443 -40 40 -40z" />
<glyph glyph-name="square" unicode="&#xf0c8;" horiz-adv-x="448"
d="M400 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352zM394 16c3.2998 0 6 2.7002 6 6v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340z" />
<glyph glyph-name="envelope" unicode="&#xf0e0;"
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM464 336h-416v-40.8047c22.4248 -18.2627 58.1797 -46.6602 134.587 -106.49
c16.834 -13.2422 50.2051 -45.0762 73.4131 -44.7012c23.2119 -0.371094 56.5723 31.4541 73.4131 44.7012c76.4189 59.8389 112.165 88.2305 134.587 106.49v40.8047zM48 48h416v185.601c-22.915 -18.252 -55.4189 -43.8691 -104.947 -82.6523
c-22.5439 -17.748 -60.3359 -55.1787 -103.053 -54.9473c-42.9277 -0.231445 -81.2051 37.75 -103.062 54.9551c-49.5293 38.7842 -82.0244 64.3945 -104.938 82.6455v-185.602z" />
<glyph glyph-name="lightbulb" unicode="&#xf0eb;" horiz-adv-x="352"
d="M176 368c8.83984 0 16 -7.16016 16 -16s-7.16016 -16 -16 -16c-35.2803 0 -64 -28.7002 -64 -64c0 -8.83984 -7.16016 -16 -16 -16s-16 7.16016 -16 16c0 52.9404 43.0596 96 96 96zM96.0596 -11.1699l-0.0400391 43.1797h159.961l-0.0507812 -43.1797
c-0.00976562 -3.13965 -0.939453 -6.21973 -2.67969 -8.83984l-24.5098 -36.8398c-2.95996 -4.45996 -7.95996 -7.14062 -13.3203 -7.14062h-78.8496c-5.35059 0 -10.3506 2.68066 -13.3203 7.14062l-24.5098 36.8398c-1.75 2.62012 -2.68066 5.68945 -2.68066 8.83984z
M176 448c97.2002 0 176 -78.7998 176 -176c0 -44.3701 -16.4502 -84.8496 -43.5498 -115.79c-16.6406 -18.9795 -42.7402 -58.79 -52.4199 -92.1602v-0.0498047h-48v0.0996094c0.00390625 4.04199 0.999023 10.4482 2.21973 14.3008
c5.67969 17.9893 22.9902 64.8496 62.0996 109.46c20.4102 23.29 31.6504 53.1699 31.6504 84.1396c0 70.5801 -57.4199 128 -128 128c-68.2803 0 -128.15 -54.3604 -127.95 -128c0.0898438 -30.9902 11.0703 -60.71 31.6104 -84.1396
c39.3496 -44.9004 56.5801 -91.8604 62.1699 -109.67c1.42969 -4.56055 2.13965 -9.30078 2.15039 -14.0703v-0.120117h-48v0.0595703c-9.68066 33.3604 -35.7803 73.1709 -52.4209 92.1602c-27.1094 30.9307 -43.5596 71.4102 -43.5596 115.78
c0 93.0303 73.7197 176 176 176z" />
<glyph glyph-name="bell" unicode="&#xf0f3;" horiz-adv-x="448"
d="M439.39 85.71c6 -6.44043 8.66016 -14.1602 8.61035 -21.71c-0.0996094 -16.4004 -12.9805 -32 -32.0996 -32h-383.801c-19.1191 0 -31.9893 15.5996 -32.0996 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c19.3193 20.7598 55.4697 51.9902 55.4697 154.29
c0 77.7002 54.4795 139.9 127.939 155.16v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -102.3 36.1504 -133.53 55.4697 -154.29zM67.5303 80h312.939
c-21.2197 27.96 -44.4199 74.3203 -44.5293 159.42c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112s-112 -50.1396 -112 -112c0 -0.200195 0.0595703 -0.379883 0.0595703 -0.580078
c-0.109375 -85.0898 -23.3096 -131.45 -44.5293 -159.42zM224 -64c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
<glyph glyph-name="hospital" unicode="&#xf0f8;" horiz-adv-x="448"
d="M128 204v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12zM268 192c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40
c0 -6.62695 -5.37305 -12 -12 -12h-40zM192 108c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40zM268 96c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40
c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.62695 5.37305 12 12 12h19.5v378.965c0 11.6172 10.7451 21.0352 24 21.0352h88.5v40c0 13.2549 10.7451 24 24 24h112c13.2549 0 24 -10.7451 24 -24v-40h88.5
c13.2549 0 24 -9.41797 24 -21.0352v-378.965h19.5c6.62695 0 12 -5.37305 12 -12zM79.5 -15h112.5v67c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-67h112.5v351h-64.5v-24c0 -13.2549 -10.7451 -24 -24 -24h-112c-13.2549 0 -24 10.7451 -24 24v24
h-64.5v-351zM266 384h-26v26c0 3.31152 -2.68848 6 -6 6h-20c-3.31152 0 -6 -2.68848 -6 -6v-26h-26c-3.31152 0 -6 -2.68848 -6 -6v-20c0 -3.31152 2.68848 -6 6 -6h26v-26c0 -3.31152 2.68848 -6 6 -6h20c3.31152 0 6 2.68848 6 6v26h26c3.31152 0 6 2.68848 6 6v20
c0 3.31152 -2.68848 6 -6 6z" />
<glyph glyph-name="plus-square" unicode="&#xf0fe;" horiz-adv-x="448"
d="M352 208v-32c0 -6.59961 -5.40039 -12 -12 -12h-88v-88c0 -6.59961 -5.40039 -12 -12 -12h-32c-6.59961 0 -12 5.40039 -12 12v88h-88c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h88v88c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12
v-88h88c6.59961 0 12 -5.40039 12 -12zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340
c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="circle" unicode="&#xf111;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200z" />
<glyph glyph-name="smile" unicode="&#xf118;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM332 135.4c8.5 10.1992 23.7002 11.5 33.7998 3.09961c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998
c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.0996 8.40039 25.2998 7.09961 33.7998 -3.09961c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004z" />
<glyph glyph-name="frown" unicode="&#xf119;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 144c40.2002 0 78 -17.7002 103.8 -48.5996c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008
c-10.7002 -8.7998 -25.7002 -6.59961 -33.7998 3.10059c-16.6006 20 -41 31.3994 -66.9004 31.3994s-50.2998 -11.5 -66.9004 -31.3994c-8.5 -10.2002 -23.5996 -11.5 -33.7998 -3.10059c-10.2002 8.5 -11.5996 23.6006 -3.09961 33.8008
c25.7998 30.8994 63.5996 48.5996 103.8 48.5996z" />
<glyph glyph-name="meh" unicode="&#xf11a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM336 128c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-176c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h176z
" />
<glyph glyph-name="keyboard" unicode="&#xf11c;" horiz-adv-x="576"
d="M528 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h480zM536 48v288c0 4.41113 -3.58887 8 -8 8h-480c-4.41113 0 -8 -3.58887 -8 -8v-288c0 -4.41113 3.58887 -8 8 -8
h480c4.41113 0 8 3.58887 8 8zM170 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM266 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM362 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM458 178c0 -6.62695 -5.37305 -12 -12 -12h-28
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 96c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 96
c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28
c6.62695 0 12 -5.37305 12 -12v-28zM218 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM314 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM410 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 260c0 -6.62695 -5.37305 -12 -12 -12h-28
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM408 102c0 -6.62695 -5.37305 -12 -12 -12h-216c-6.62695 0 -12 5.37305 -12 12v16c0 6.62695 5.37305 12 12 12h216c6.62695 0 12 -5.37305 12 -12v-16z" />
<glyph glyph-name="calendar" unicode="&#xf133;" horiz-adv-x="448"
d="M400 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12
v-52h48zM394 -16c3.2998 0 6 2.7002 6 6v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340z" />
<glyph glyph-name="play-circle" unicode="&#xf144;"
d="M371.7 210c16.3994 -9.2002 16.3994 -32.9004 0 -42l-176 -101c-15.9004 -8.7998 -35.7002 2.59961 -35.7002 21v208c0 18.5 19.9004 29.7998 35.7002 21zM504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192
c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200z" />
<glyph glyph-name="minus-square" unicode="&#xf146;" horiz-adv-x="448"
d="M108 164c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h232c6.59961 0 12 -5.40039 12 -12v-32c0 -6.59961 -5.40039 -12 -12 -12h-232zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352
c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="check-square" unicode="&#xf14a;" horiz-adv-x="448"
d="M400 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h352zM400 16v352h-352v-352h352zM364.136 257.724l-172.589 -171.204
c-4.70508 -4.66699 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66699 4.70508 -4.63672 12.3037 0.0693359 16.9717l22.7188 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l59.792 -60.2773l141.353 140.217
c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0683594l22.5361 -22.7178c4.66699 -4.70605 4.63672 -12.3047 -0.0683594 -16.9717z" />
<glyph glyph-name="share-square" unicode="&#xf14d;" horiz-adv-x="576"
d="M561.938 289.94c18.75 -18.7402 18.75 -49.1406 0 -67.8809l-143.998 -144c-29.9727 -29.9727 -81.9404 -9.05273 -81.9404 33.9404v53.7998c-101.266 -7.83691 -99.625 -31.6406 -84.1104 -78.7598c14.2285 -43.0889 -33.4736 -79.248 -71.0195 -55.7402
c-51.6924 32.3057 -84.8701 83.0635 -84.8701 144.76c0 39.3408 12.2197 72.7402 36.3301 99.3008c19.8398 21.8398 47.7402 38.4697 82.9102 49.4199c36.7295 11.4395 78.3096 16.1094 120.76 17.9893v57.1982c0 42.9355 51.9258 63.9541 81.9404 33.9404zM384 112l144 144
l-144 144v-104.09c-110.86 -0.90332 -240 -10.5166 -240 -119.851c0 -52.1396 32.79 -85.6094 62.3096 -104.06c-39.8174 120.65 48.999 141.918 177.69 143.84v-103.84zM408.74 27.5068c6.14844 1.75684 15.5449 5.92383 20.9736 9.30273
c7.97656 4.95215 18.2861 -0.825195 18.2861 -10.2139v-42.5957c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h132c6.62695 0 12 -5.37305 12 -12v-4.48633c0 -4.91699 -2.9873 -9.36914 -7.56934 -11.1514
c-13.7021 -5.33105 -26.3955 -11.5371 -38.0498 -18.585c-1.59668 -0.974609 -4.41016 -1.77051 -6.28027 -1.77734h-86.1006c-3.31152 0 -6 -2.68848 -6 -6v-340c0 -3.31152 2.68848 -6 6 -6h340c3.31152 0 6 2.68848 6 6v25.9658c0 5.37012 3.5791 10.0596 8.74023 11.541
z" />
<glyph glyph-name="compass" unicode="&#xf14e;" horiz-adv-x="496"
d="M347.94 318.14c16.6592 7.61035 33.8096 -9.54004 26.1992 -26.1992l-65.9697 -144.341c-2.73047 -5.97363 -9.7959 -13.0391 -15.7695 -15.7695l-144.341 -65.9697c-16.6592 -7.61035 -33.8096 9.5498 -26.1992 26.1992l65.9697 144.341
c2.73047 5.97363 9.7959 13.0391 15.7695 15.7695zM270.58 169.42c12.4697 12.4697 12.4697 32.6904 0 45.1602s-32.6904 12.4697 -45.1602 0s-12.4697 -32.6904 0 -45.1602s32.6904 -12.4697 45.1602 0zM248 440c136.97 0 248 -111.03 248 -248s-111.03 -248 -248 -248
s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
<glyph glyph-name="caret-square-down" unicode="&#xf150;" horiz-adv-x="448"
d="M125.1 240h197.801c10.6992 0 16.0996 -13 8.5 -20.5l-98.9004 -98.2998c-4.7002 -4.7002 -12.2002 -4.7002 -16.9004 0l-98.8994 98.2998c-7.7002 7.5 -2.2998 20.5 8.39941 20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="caret-square-up" unicode="&#xf151;" horiz-adv-x="448"
d="M322.9 144h-197.801c-10.6992 0 -16.0996 13 -8.5 20.5l98.9004 98.2998c4.7002 4.7002 12.2002 4.7002 16.9004 0l98.8994 -98.2998c7.7002 -7.5 2.2998 -20.5 -8.39941 -20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="caret-square-right" unicode="&#xf152;" horiz-adv-x="448"
d="M176 93.0996v197.801c0 10.6992 13 16.0996 20.5 8.5l98.2998 -98.9004c4.7002 -4.7002 4.7002 -12.2002 0 -16.9004l-98.2998 -98.8994c-7.5 -7.7002 -20.5 -2.2998 -20.5 8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="file" unicode="&#xf15b;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416z" />
<glyph glyph-name="file-alt" unicode="&#xf15c;" horiz-adv-x="384"
d="M288 200v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v28c0 6.59961 5.40039 12 12 12h168c6.59961 0 12 -5.40039 12 -12zM276 128c6.59961 0 12 -5.40039 12 -12v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12
v28c0 6.59961 5.40039 12 12 12h168zM384 316.1v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996l83.9004 -83.9004c9 -8.90039 14.0996 -21.2002 14.0996 -33.9004z
M256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288z" />
<glyph glyph-name="thumbs-up" unicode="&#xf164;"
d="M466.27 161.31c4.6748 -22.6465 0.864258 -44.5371 -8.98926 -62.9893c2.95898 -23.8682 -4.02148 -48.5654 -17.3398 -66.9902c-0.954102 -55.9072 -35.8232 -95.3301 -112.94 -95.3301c-7 0 -15 0.00976562 -22.2197 0.00976562
c-102.742 0 -133.293 38.9395 -177.803 39.9404c-3.56934 -13.7764 -16.085 -23.9502 -30.9775 -23.9502h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h98.7598c19.1455 16.9531 46.0137 60.6533 68.7598 83.4004
c13.667 13.667 10.1533 108.6 71.7607 108.6c57.5801 0 95.2695 -31.9355 95.2695 -104.73c0 -18.4092 -3.92969 -33.7295 -8.84961 -46.5391h36.4795c48.6025 0 85.8203 -41.5654 85.8203 -85.5801c0 -19.1504 -4.95996 -34.9902 -13.7305 -49.8408zM404.52 107.48
c21.5811 20.3838 18.6992 51.0645 5.21094 65.6191c9.44922 0 22.3594 18.9102 22.2695 37.8105c-0.0898438 18.9102 -16.71 37.8203 -37.8203 37.8203h-103.989c0 37.8193 28.3594 55.3691 28.3594 94.5391c0 23.75 0 56.7305 -47.2695 56.7305
c-18.9102 -18.9102 -9.45996 -66.1797 -37.8203 -94.54c-26.5596 -26.5703 -66.1797 -97.46 -94.54 -97.46h-10.9199v-186.17c53.6113 0 100.001 -37.8203 171.64 -37.8203h37.8203c35.5117 0 60.8203 17.1201 53.1201 65.9004
c15.2002 8.16016 26.5 36.4395 13.9395 57.5703zM88 16c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
<glyph glyph-name="thumbs-down" unicode="&#xf165;"
d="M466.27 222.69c8.77051 -14.8506 13.7305 -30.6904 13.7305 -49.8408c0 -44.0146 -37.2178 -85.5801 -85.8203 -85.5801h-36.4795c4.91992 -12.8096 8.84961 -28.1299 8.84961 -46.5391c0 -72.7949 -37.6895 -104.73 -95.2695 -104.73
c-61.6074 0 -58.0938 94.9326 -71.7607 108.6c-22.7461 22.7471 -49.6133 66.4473 -68.7598 83.4004h-7.05176c-5.5332 -9.56152 -15.8662 -16 -27.708 -16h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h64c8.11328 0 15.5146 -3.02539 21.1553 -8
h10.8447c40.9971 0 73.1953 39.9902 176.78 39.9902c7.21973 0 15.2197 0.00976562 22.2197 0.00976562c77.1172 0 111.986 -39.4229 112.94 -95.3301c13.3184 -18.4248 20.2979 -43.1221 17.3398 -66.9902c9.85352 -18.4521 13.6641 -40.3428 8.98926 -62.9893zM64 152
c13.2549 0 24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24zM394.18 135.27c21.1104 0 37.7305 18.9102 37.8203 37.8203c0.0898438 18.9004 -12.8203 37.8105 -22.2695 37.8105c13.4883 14.5547 16.3701 45.2354 -5.21094 65.6191
c12.5605 21.1309 1.26074 49.4102 -13.9395 57.5703c7.7002 48.7803 -17.6084 65.9004 -53.1201 65.9004h-37.8203c-71.6387 0 -118.028 -37.8203 -171.64 -37.8203v-186.17h10.9199c28.3604 0 67.9805 -70.8896 94.54 -97.46
c28.3604 -28.3604 18.9102 -75.6299 37.8203 -94.54c47.2695 0 47.2695 32.9805 47.2695 56.7305c0 39.1699 -28.3594 56.7197 -28.3594 94.5391h103.989z" />
<glyph glyph-name="sun" unicode="&#xf185;"
d="M494.2 226.1c11.2002 -7.59961 17.7998 -20.0996 17.8994 -33.6992c0 -13.4004 -6.69922 -26 -17.7998 -33.5l-59.7998 -40.5l13.7002 -71c2.5 -13.2002 -1.60059 -26.8008 -11.1006 -36.3008s-22.8994 -13.7998 -36.2998 -11.0996l-70.8994 13.7002l-40.4004 -59.9004
c-7.5 -11.0996 -20.0996 -17.7998 -33.5 -17.7998s-26 6.7002 -33.5 17.9004l-40.4004 59.8994l-70.7998 -13.7002c-13.3994 -2.59961 -26.7998 1.60059 -36.2998 11.1006s-13.7002 23.0996 -11.0996 36.2998l13.6992 71l-59.7998 40.5
c-11.0996 7.5 -17.7998 20 -17.7998 33.5s6.59961 26 17.7998 33.5996l59.7998 40.5l-13.6992 71c-2.60059 13.2002 1.59961 26.7002 11.0996 36.3008c9.5 9.59961 23 13.6992 36.2998 11.1992l70.7998 -13.6992l40.4004 59.8994c15.0996 22.2998 51.9004 22.2998 67 0
l40.4004 -59.8994l70.8994 13.6992c13 2.60059 26.6006 -1.59961 36.2002 -11.0996c9.5 -9.59961 13.7002 -23.2002 11.0996 -36.4004l-13.6992 -71zM381.3 140.5l76.7998 52.0996l-76.7998 52l17.6006 91.1006l-91 -17.6006l-51.9004 76.9004l-51.7998 -76.7998
l-91 17.5996l17.5996 -91.2002l-76.7998 -52l76.7998 -52l-17.5996 -91.1992l90.8994 17.5996l51.9004 -77l51.9004 76.9004l91 -17.6006zM256 296c57.2998 0 104 -46.7002 104 -104s-46.7002 -104 -104 -104s-104 46.7002 -104 104s46.7002 104 104 104zM256 136
c30.9004 0 56 25.0996 56 56s-25.0996 56 -56 56s-56 -25.0996 -56 -56s25.0996 -56 56 -56z" />
<glyph glyph-name="moon" unicode="&#xf186;"
d="M279.135 -64c-141.424 0 -256 114.64 -256 256c0 141.425 114.641 256 256 256c13.0068 -0.00195312 33.9443 -1.91797 46.7354 -4.27734c44.0205 -8.13086 53.7666 -66.8691 15.0215 -88.9189c-41.374 -23.5439 -67.4336 -67.4121 -67.4336 -115.836
c0 -83.5234 75.9238 -146.475 158.272 -130.792c43.6904 8.32129 74.5186 -42.5693 46.248 -77.4004c-47.8613 -58.9717 -120.088 -94.7754 -198.844 -94.7754zM279.135 400c-114.875 0 -208 -93.125 -208 -208s93.125 -208 208 -208
c65.2314 0 123.439 30.0361 161.575 77.0244c-111.611 -21.2568 -215.252 64.0957 -215.252 177.943c0 67.5127 36.9326 126.392 91.6934 157.555c-12.3271 2.27637 -25.0312 3.47754 -38.0166 3.47754z" />
<glyph glyph-name="caret-square-left" unicode="&#xf191;" horiz-adv-x="448"
d="M272 290.9v-197.801c0 -10.6992 -13 -16.0996 -20.5 -8.5l-98.2998 98.9004c-4.7002 4.7002 -4.7002 12.2002 0 16.9004l98.2998 98.8994c7.5 7.7002 20.5 2.2998 20.5 -8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="dot-circle" unicode="&#xf192;"
d="M256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200zM256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248z
M256 272c44.1826 0 80 -35.8174 80 -80s-35.8174 -80 -80 -80s-80 35.8174 -80 80s35.8174 80 80 80z" />
<glyph glyph-name="building" unicode="&#xf1ad;" horiz-adv-x="448"
d="M128 300v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12zM268 288c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40
c0 -6.59961 -5.40039 -12 -12 -12h-40zM140 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM268 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM192 108c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM268 96c-6.59961 0 -12 5.40039 -12 12v40
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.59961 5.40039 12 12 12h19.5v440c0 13.2998 10.7002 24 24 24h337c13.2998 0 24 -10.7002 24 -24v-440h19.5
c6.59961 0 12 -5.40039 12 -12zM79.5 -15h112.5v67c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-67h112.5v414l-288.5 1z" />
<glyph glyph-name="file-pdf" unicode="&#xf1c1;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM298.2 127.7c10.5 -10.5 8 -38.7002 -17.5 -38.7002c-14.7998 0 -36.9004 6.7998 -55.7998 17c-21.6006 -3.59961 -46 -12.7002 -68.4004 -20.0996c-50.0996 -86.4004 -79.4004 -47 -76.0996 -31.2002
c4 20 31 35.8994 51 46.2002c10.5 18.3994 25.3994 50.5 35.3994 74.3994c-7.39941 28.6006 -11.3994 51 -7 67.1006c4.7998 17.6992 38.4004 20.2998 42.6006 -5.90039c4.69922 -15.4004 -1.5 -39.9004 -5.40039 -56c8.09961 -21.2998 19.5996 -35.7998 36.7998 -46.2998
c17.4004 2.2002 52.2002 5.5 64.4004 -6.5zM100.1 49.9004c0 -0.700195 11.4004 4.69922 30.4004 35c-5.90039 -5.5 -25.2998 -21.3008 -30.4004 -35zM181.7 240.5c-2.5 0 -2.60059 -26.9004 1.7998 -40.7998c4.90039 8.7002 5.59961 40.7998 -1.7998 40.7998zM157.3 103.9
c15.9004 6.09961 34 14.8994 54.7998 19.1992c-11.1992 8.30078 -21.7998 20.4004 -30.0996 35.5c-6.7002 -17.6992 -15 -37.7998 -24.7002 -54.6992zM288.9 108.9c3.59961 2.39941 -2.2002 10.3994 -37.3008 7.7998c32.3008 -13.7998 37.3008 -7.7998 37.3008 -7.7998z" />
<glyph glyph-name="file-word" unicode="&#xf1c2;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM268.1 192v0.200195h15.8008c7.7998 0 13.5 -7.2998 11.5996 -14.9004c-4.2998 -17 -13.7002 -54.0996 -34.5 -136c-1.2998 -5.39941 -6.09961 -9.09961 -11.5996 -9.09961h-24.7002
c-5.5 0 -10.2998 3.7998 -11.6006 9.09961c-5.2998 20.9004 -17.7998 71 -17.8994 71.4004l-2.90039 17.2998c-0.5 -5.2998 -1.5 -11.0996 -3 -17.2998l-17.8994 -71.4004c-1.30078 -5.39941 -6.10059 -9.09961 -11.6006 -9.09961h-25.2002
c-5.59961 0 -10.3994 3.7002 -11.6992 9.09961c-6.5 26.5 -25.2002 103.4 -33.2002 136c-1.7998 7.5 3.89941 14.7998 11.7002 14.7998h16.7998c5.7998 0 10.7002 -4.09961 11.7998 -9.69922c5 -25.7002 18.4004 -93.8008 19.0996 -99
c0.300781 -1.7002 0.400391 -3.10059 0.5 -4.2002c0.800781 7.5 0.400391 4.7002 24.8008 103.7c1.39941 5.2998 6.19922 9.09961 11.6992 9.09961h13.3008c5.59961 0 10.3994 -3.7998 11.6992 -9.2002c23.9004 -99.7002 22.8008 -94.3994 23.6006 -99.5
c0.299805 -1.7002 0.5 -3.09961 0.700195 -4.2998c0.599609 8.09961 0.399414 5.7998 21 103.5c1.09961 5.5 6 9.5 11.6992 9.5z" />
<glyph glyph-name="file-excel" unicode="&#xf1c3;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM260 224c9.2002 0 15 -10 10.2998 -18c-16 -27.5 -45.5996 -76.9004 -46.2998 -78l46.4004 -78c4.59961 -8 -1.10059 -18 -10.4004 -18h-28.7998c-4.40039 0 -8.5 2.40039 -10.6006 6.2998
c-22.6992 41.7998 -13.6992 27.5 -28.5996 57.7002c-5.59961 -12.7002 -6.90039 -17.7002 -28.5996 -57.7002c-2.10059 -3.89941 -6.10059 -6.2998 -10.5 -6.2998h-28.9004c-9.2998 0 -15.0996 10 -10.4004 18l46.3008 78l-46.3008 78c-4.59961 8 1.10059 18 10.4004 18
h28.9004c4.39941 0 8.5 -2.40039 10.5996 -6.2998c21.7002 -40.4004 14.7002 -28.6006 28.5996 -57.7002c6.40039 15.2998 10.6006 24.5996 28.6006 57.7002c2.09961 3.89941 6.09961 6.2998 10.5 6.2998h28.7998z" />
<glyph glyph-name="file-powerpoint" unicode="&#xf1c4;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM120 44v168c0 6.59961 5.40039 12 12 12h69.2002c36.7002 0 62.7998 -27 62.7998 -66.2998c0 -74.2998 -68.7002 -66.5 -95.5 -66.5v-47.2002c0 -6.59961 -5.40039 -12 -12 -12h-24.5c-6.59961 0 -12 5.40039 -12 12z
M168.5 131.4h23c7.90039 0 13.9004 2.39941 18.0996 7.19922c8.5 9.80078 8.40039 28.5 0.100586 37.8008c-4.10059 4.59961 -9.90039 7 -17.4004 7h-23.8994v-52h0.0996094z" />
<glyph glyph-name="file-image" unicode="&#xf1c5;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM80 32v64l39.5 39.5c4.7002 4.7002 12.2998 4.7002 17 0l39.5 -39.5l87.5 87.5c4.7002 4.7002 12.2998 4.7002 17 0l23.5 -23.5v-128h-224zM128 272c26.5 0 48 -21.5 48 -48s-21.5 -48 -48 -48s-48 21.5 -48 48
s21.5 48 48 48z" />
<glyph glyph-name="file-archive" unicode="&#xf1c6;" horiz-adv-x="384"
d="M128.3 288h32v-32h-32v32zM192.3 384v-32h-32v32h32zM128.3 352h32v-32h-32v32zM192.3 320v-32h-32v32h32zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1
c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-48.2998v-16h-32v16h-79.7002v-416h288zM194.2 182.3l17.2998 -87.7002c6.40039 -32.3994 -18.4004 -62.5996 -51.5 -62.5996
c-33.2002 0 -58 30.4004 -51.4004 62.9004l19.7002 97.0996v32h32v-32h22.1006c5.7998 0 10.6992 -4.09961 11.7998 -9.7002zM160.3 57.9004c17.9004 0 32.4004 12.0996 32.4004 27c0 14.8994 -14.5 27 -32.4004 27c-17.8994 0 -32.3994 -12.1006 -32.3994 -27
c0 -14.9004 14.5 -27 32.3994 -27zM192.3 256v-32h-32v32h32z" />
<glyph glyph-name="file-audio" unicode="&#xf1c7;" horiz-adv-x="384"
d="M369.941 350.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM332.118 320
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM192 60.0244c0 -10.6914 -12.9258 -16.0459 -20.4854 -8.48535l-35.5146 35.9746h-28c-6.62695 0 -12 5.37305 -12 12v56c0 6.62695 5.37305 12 12 12h28
l35.5146 36.9473c7.56055 7.56055 20.4854 2.20605 20.4854 -8.48535v-135.951zM233.201 107.154c9.05078 9.29688 9.05957 24.1328 0.000976562 33.4385c-22.1494 22.752 12.2344 56.2461 34.3945 33.4814c27.1982 -27.9404 27.2119 -72.4443 0.000976562 -100.401
c-21.793 -22.3857 -56.9463 10.3154 -34.3965 33.4814z" />
<glyph glyph-name="file-video" unicode="&#xf1c8;" horiz-adv-x="384"
d="M369.941 350.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM332.118 320
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM276.687 195.303c10.0049 10.0049 27.3135 2.99707 27.3135 -11.3135v-111.976c0 -14.2939 -17.2959 -21.332 -27.3135 -11.3135l-52.6865 52.6738v-37.374
c0 -11.0459 -8.9541 -20 -20 -20h-104c-11.0459 0 -20 8.9541 -20 20v104c0 11.0459 8.9541 20 20 20h104c11.0459 0 20 -8.9541 20 -20v-37.374z" />
<glyph glyph-name="file-code" unicode="&#xf1c9;" horiz-adv-x="384"
d="M149.9 98.9004c3.5 -3.30078 3.69922 -8.90039 0.399414 -12.4004l-17.3994 -18.5996c-1.60059 -1.80078 -4 -2.80078 -6.40039 -2.80078c-2.2002 0 -4.40039 0.900391 -6 2.40039l-57.7002 54.0996c-3.7002 3.40039 -3.7002 9.30078 0 12.8008l57.7002 54.0996
c3.40039 3.2998 9 3.2002 12.4004 -0.400391l17.3994 -18.5996l0.200195 -0.200195c3.2002 -3.59961 2.7998 -9.2002 -0.799805 -12.3994l-32.7998 -28.9004l32.7998 -28.9004zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288
c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288zM209.6 234l24.4004 -7
c4.7002 -1.2998 7.40039 -6.2002 6 -10.9004l-54.7002 -188.199c-1.2998 -4.60059 -6.2002 -7.40039 -10.8994 -6l-24.4004 7.09961c-4.7002 1.2998 -7.40039 6.2002 -6 10.9004l54.7002 188.1c1.39941 4.7002 6.2002 7.40039 10.8994 6zM234.1 157.1
c-3.5 3.30078 -3.69922 8.90039 -0.399414 12.4004l17.3994 18.5996c3.30078 3.60059 8.90039 3.7002 12.4004 0.400391l57.7002 -54.0996c3.7002 -3.40039 3.7002 -9.30078 0 -12.8008l-57.7002 -54.0996c-3.5 -3.2998 -9.09961 -3.09961 -12.4004 0.400391
l-17.3994 18.5996l-0.200195 0.200195c-3.2002 3.59961 -2.7998 9.2002 0.799805 12.3994l32.7998 28.9004l-32.7998 28.9004z" />
<glyph glyph-name="life-ring" unicode="&#xf1cd;"
d="M256 -56c-136.967 0 -248 111.033 -248 248s111.033 248 248 248s248 -111.033 248 -248s-111.033 -248 -248 -248zM152.602 20.7197c63.2178 -38.3184 143.579 -38.3184 206.797 0l-53.4111 53.4111c-31.8467 -13.5215 -68.168 -13.5059 -99.9746 0zM336 192
c0 44.1123 -35.8877 80 -80 80s-80 -35.8877 -80 -80s35.8877 -80 80 -80s80 35.8877 80 80zM427.28 88.6016c38.3184 63.2178 38.3184 143.579 0 206.797l-53.4111 -53.4111c13.5215 -31.8467 13.5049 -68.168 0 -99.9746zM359.397 363.28
c-63.2168 38.3184 -143.578 38.3184 -206.796 0l53.4111 -53.4111c31.8457 13.5215 68.167 13.5049 99.9736 0zM84.7197 295.398c-38.3184 -63.2178 -38.3184 -143.579 0 -206.797l53.4111 53.4111c-13.5215 31.8467 -13.5059 68.168 0 99.9746z" />
<glyph glyph-name="paper-plane" unicode="&#xf1d8;"
d="M440 441.5c34.5996 19.9004 77.5996 -8.7998 71.5 -48.9004l-59.4004 -387.199c-2.2998 -14.5 -11.0996 -27.3008 -23.8994 -34.5c-7.2998 -4.10059 -15.4004 -6.2002 -23.6006 -6.2002c-6.19922 0 -12.3994 1.2002 -18.2998 3.59961l-111.899 46.2002l-43.8008 -59.0996
c-27.3994 -36.9004 -86.5996 -17.8008 -86.5996 28.5996v84.4004l-114.3 47.2998c-36.7998 15.0996 -40.1006 66 -5.7002 85.8994zM192 -16l36.5996 49.5l-36.5996 15.0996v-64.5996zM404.6 12.7002l59.4004 387.3l-416 -240l107.8 -44.5996l211.5 184.3
c14.2002 12.2998 34.4004 -5.7002 23.7002 -21.2002l-140.2 -202.3z" />
<glyph glyph-name="futbol" unicode="&#xf1e3;" horiz-adv-x="496"
d="M483.8 268.6c42.2998 -130.199 -29 -270.1 -159.2 -312.399c-25.5 -8.2998 -51.2998 -12.2002 -76.6992 -12.2002c-104.5 0 -201.7 66.5996 -235.7 171.4c-42.2998 130.199 29 270.1 159.2 312.399c25.5 8.2998 51.2998 12.2002 76.6992 12.2002
c104.5 0 201.7 -66.5996 235.7 -171.4zM409.3 74.9004c6.10059 8.39941 12.1006 16.8994 16.7998 26.1992c14.3008 28.1006 21.5 58.5 21.7002 89.2002l-38.8994 36.4004l-71.1006 -22.1006l-24.3994 -75.1992l43.6992 -60.9004zM409.3 310.3
c-24.5 33.4004 -58.7002 58.4004 -97.8994 71.4004l-47.4004 -26.2002v-73.7998l64.2002 -46.5l70.7002 22zM184.9 381.6c-39.9004 -13.2998 -73.5 -38.5 -97.8008 -71.8994l10.1006 -52.5l70.5996 -22l64.2002 46.5v73.7998zM139 68.5l43.5 61.7002l-24.2998 74.2998
l-71.1006 22.2002l-39 -36.4004c0.5 -55.7002 23.4004 -95.2002 37.8008 -115.3zM187.2 1.5c64.0996 -20.4004 115.5 -1.7998 121.7 0l22.3994 48.0996l-44.2998 61.7002h-78.5996l-43.6006 -61.7002z" />
<glyph glyph-name="newspaper" unicode="&#xf1ea;" horiz-adv-x="576"
d="M552 384c13.2549 0 24 -10.7451 24 -24v-336c0 -13.2549 -10.7451 -24 -24 -24h-496c-30.9277 0 -56 25.0723 -56 56v272c0 13.2549 10.7451 24 24 24h42.752c6.60547 18.623 24.3896 32 45.248 32h440zM48 56c0 -4.41113 3.58887 -8 8 -8s8 3.58887 8 8v248h-16v-248z
M528 48v288h-416v-280c0 -2.7168 -0.204102 -5.38574 -0.578125 -8h416.578zM172 168c-6.62695 0 -12 5.37305 -12 12v96c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-96c0 -6.62695 -5.37305 -12 -12 -12h-136zM200 248v-40h80v40h-80zM160 108v24
c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-136c-6.62695 0 -12 5.37305 -12 12zM352 108v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104
c-6.62695 0 -12 5.37305 -12 12zM352 252v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12zM352 180v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24
c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12z" />
<glyph glyph-name="bell-slash" unicode="&#xf1f6;" horiz-adv-x="640"
d="M633.99 -23.0195c6.91016 -5.52051 8.01953 -15.5908 2.5 -22.4902l-10 -12.4902c-5.53027 -6.88965 -15.5898 -8.00977 -22.4902 -2.49023l-598 467.51c-6.90039 5.52051 -8.01953 15.5908 -2.49023 22.4902l10 12.4902
c5.52051 6.90039 15.5898 8.00977 22.4902 2.49023zM163.53 80h182.84l61.3994 -48h-279.659c-19.1201 0 -31.9902 15.5996 -32.1006 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c18.3701 19.7402 51.5703 49.6904 54.8398 140.42l45.4697 -35.5498
c-6.91992 -54.7803 -24.6895 -88.5498 -41.3994 -110.58zM320 352c-23.3496 0 -45 -7.17969 -62.9404 -19.4004l-38.1699 29.8408c19.6807 15.7793 43.1104 27.3096 69.1299 32.7197v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398
c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -41.3604 6.03027 -70.7197 14.3398 -92.8496l-59.5293 46.54c-1.63086 13.96 -2.77051 28.8896 -2.79004 45.7295c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112zM320 -64
c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
<glyph glyph-name="copyright" unicode="&#xf1f9;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
M363.351 93.0645c-9.61328 -9.71289 -45.5293 -41.3965 -104.064 -41.3965c-82.4297 0 -140.484 61.4248 -140.484 141.567c0 79.1514 60.2754 139.4 139.763 139.4c55.5303 0 88.7373 -26.6201 97.5928 -34.7783c2.13379 -1.96289 3.86523 -5.9082 3.86523 -8.80762
c0 -1.95508 -0.864258 -4.87402 -1.92969 -6.51465l-18.1543 -28.1133c-3.8418 -5.9502 -11.9668 -7.28223 -17.499 -2.9209c-8.5957 6.77637 -31.8145 22.5381 -61.708 22.5381c-48.3037 0 -77.916 -35.3301 -77.916 -80.082c0 -41.5889 26.8877 -83.6924 78.2764 -83.6924
c32.6572 0 56.8428 19.0391 65.7266 27.2256c5.26953 4.85645 13.5957 4.03906 17.8193 -1.73828l19.8652 -27.1699c1.28613 -1.74512 2.33008 -4.91992 2.33008 -7.08789c0 -2.72363 -1.56055 -6.5 -3.48242 -8.42969z" />
<glyph glyph-name="closed-captioning" unicode="&#xf20a;"
d="M464 384c26.5 0 48 -21.5 48 -48v-288c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h416zM458 48c3.2998 0 6 2.7002 6 6v276c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-276c0 -3.2998 2.7002 -6 6 -6h404z
M246.9 133.7c1.69922 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.6006 -56.7998 -172.801 -32.0996 -172.801 67.9004c0 97.2998 121.7 119.5 172.5 70.0996c2.10059 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.89941 -3.09961 -6.19922 -4 -9.09961 -1.7002
c-40.7998 32 -94.5996 14.9004 -94.5996 -31.1992c0 -48 51 -70.5 92.1992 -32.6006c2.80078 2.5 7.10059 2.10059 9.2002 -0.899414zM437.3 133.7c1.7002 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.5996 -56.9004 -172.8 -32.0996 -172.8 67.9004
c0 97.2998 121.7 119.5 172.5 70.0996c2.09961 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.90039 -3.09961 -6.2002 -4 -9.09961 -1.7002c-40.8008 32 -94.6006 14.9004 -94.6006 -31.1992c0 -48 51 -70.5 92.2002 -32.6006c2.7998 2.5 7.09961 2.10059 9.2002 -0.899414z
" />
<glyph glyph-name="object-group" unicode="&#xf247;"
d="M500 320h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v256h-12
c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM448 384v-32h32v32h-32zM32 384v-32h32v32h-32zM64 0v32
h-32v-32h32zM480 0v32h-32v-32h32zM440 64v256h-12c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h12zM404 256c6.62695 0 12 -5.37207 12 -12v-168
c0 -6.62793 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37207 -12 12v52h-84c-6.62695 0 -12 5.37207 -12 12v168c0 6.62793 5.37305 12 12 12h200c6.62695 0 12 -5.37207 12 -12v-52h84zM136 280v-112h144v112h-144zM376 104v112h-56v-76
c0 -6.62793 -5.37305 -12 -12 -12h-76v-24h144z" />
<glyph glyph-name="object-ungroup" unicode="&#xf248;" horiz-adv-x="576"
d="M564 224h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v24h-88v-12
c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h72
c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-12v-24h88v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM352 384v-32h32v32h-32zM352 128v-32h32v32h-32zM64 96v32h-32v-32h32zM64 352v32
h-32v-32h32zM96 136h224v12c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-12zM224 0v32h-32v-32h32zM504 64v160h-12c-6.62695 0 -12 5.37305 -12 12v12
h-88v-88h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-88v-24h12c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h12zM544 0v32h-32v-32h32zM544 256v32h-32v-32h32z" />
<glyph glyph-name="sticky-note" unicode="&#xf249;" horiz-adv-x="448"
d="M448 99.8936c0 -10.9746 -6.29883 -26.1797 -14.0586 -33.9404l-83.8828 -83.8818c-7.75977 -7.76074 -22.9658 -14.0596 -33.9404 -14.0596h-268.118c-26.5098 0 -48 21.4902 -48 48v351.988c0 26.5098 21.4902 48 48 48h352c26.5098 0 48 -21.4902 48 -48v-268.106z
M320 19.8936l76.1182 76.1182h-76.1182v-76.1182zM400 368h-352v-351.988h224v104c0 13.2549 10.7451 24 24 24h104v223.988z" />
<glyph glyph-name="clone" unicode="&#xf24d;"
d="M464 448c26.5098 0 48 -21.4902 48 -48v-320c0 -26.5098 -21.4902 -48 -48 -48h-48v-48c0 -26.5098 -21.4902 -48 -48 -48h-320c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h48v48c0 26.5098 21.4902 48 48 48h320zM362 -16c3.31152 0 6 2.68848 6 6
v42h-224c-26.5098 0 -48 21.4902 -48 48v224h-42c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308zM458 80c3.31152 0 6 2.68848 6 6v308c0 3.31152 -2.68848 6 -6 6h-308c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308z" />
<glyph glyph-name="hourglass" unicode="&#xf254;" horiz-adv-x="384"
d="M368 400c0 -80.0996 -31.8984 -165.619 -97.1797 -208c64.9912 -42.1934 97.1797 -127.436 97.1797 -208h4c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-360c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h4
c0 80.0996 31.8994 165.619 97.1797 208c-64.9912 42.1934 -97.1797 127.436 -97.1797 208h-4c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h360c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-4zM64 400
c0 -101.621 57.3066 -184 128 -184s128 82.3799 128 184h-256zM320 -16c0 101.62 -57.3076 184 -128 184s-128 -82.3799 -128 -184h256z" />
<glyph glyph-name="hand-rock" unicode="&#xf255;"
d="M408.864 368.948c48.8213 20.751 103.136 -15.0723 103.136 -67.9111v-114.443c0 -15.3955 -3.08887 -30.3906 -9.18262 -44.5674l-42.835 -99.6562c-4.99707 -11.625 -3.98242 -18.8574 -3.98242 -42.3701c0 -17.6729 -14.3271 -32 -32 -32h-252
c-17.6729 0 -32 14.3271 -32 32c0 27.3301 1.1416 29.2012 -3.11035 32.9033l-97.71 85.0811c-24.8994 21.6797 -39.1797 52.8926 -39.1797 85.6338v56.9531c0 47.4277 44.8457 82.0215 91.0459 71.1807c1.96094 55.751 63.5107 87.8262 110.671 60.8057
c29.1895 31.0713 78.8604 31.4473 108.334 -0.0214844c32.7051 18.6846 76.4121 10.3096 98.8135 -23.5879zM464 186.594v114.445c0 34.29 -52 33.8232 -52 0.676758c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v26.751
c0 34.457 -52 33.707 -52 0.676758v-27.4287c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v40.4658c0 34.3525 -52 33.8115 -52 0.677734v-41.1436c0 -8.83594 -7.16406 -16 -16 -16h-7c-8.83594 0 -16 7.16406 -16 16v26.751
c0 34.4023 -52 33.7744 -52 0.676758v-116.571c0 -8.83203 -7.16797 -16 -16 -16c-3.30664 0 -8.01367 1.7627 -10.5068 3.93359l-7 6.09473c-3.03223 2.64062 -5.49316 8.04688 -5.49316 12.0674v0v41.2275c0 34.2148 -52 33.8857 -52 0.677734v-56.9531
c0 -18.8555 8.27441 -36.874 22.7002 -49.4365l97.71 -85.0801c12.4502 -10.8398 19.5898 -26.4463 19.5898 -42.8164v-10.2861h220v7.07617c0 13.21 2.65332 26.0791 7.88281 38.25l42.835 99.6553c2.91602 6.75391 5.28223 18.207 5.28223 25.5635v0.0488281z" />
<glyph glyph-name="hand-paper" unicode="&#xf256;" horiz-adv-x="448"
d="M372.57 335.359c39.9062 5.63281 75.4297 -25.7393 75.4297 -66.3594v-131.564c-0.00195312 -12.7666 -2.33008 -33.2246 -5.19531 -45.666l-30.1836 -130.958c-3.34668 -14.5234 -16.2783 -24.8125 -31.1816 -24.8125h-222.897
c-9.10352 0 -20.7793 6.01758 -26.0615 13.4316l-119.97 168.415c-21.2441 29.8203 -14.8047 71.3574 14.5498 93.1533c18.7754 13.9395 42.1309 16.2979 62.083 8.87109v126.13c0 44.0547 41.125 75.5439 82.4053 64.9834c23.8926 48.1963 92.3535 50.2471 117.982 0.74707
c42.5186 11.1445 83.0391 -21.9346 83.0391 -65.5469v-10.8242zM399.997 137.437l-0.00195312 131.563c0 24.9492 -36.5703 25.5508 -36.5703 -0.691406v-76.3086c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v154.184
c0 25.501 -36.5703 26.3633 -36.5703 0.691406v-154.875c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v188.309c0 25.501 -36.5703 26.3545 -36.5703 0.691406v-189c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16
v153.309c0 25.501 -36.5713 26.3359 -36.5713 0.691406v-206.494c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832l-27.1279 38.0791c-14.3711 20.1709 -43.833 -2.33496 -29.3945 -22.6045l115.196 -161.697h201.92l27.3252 118.551
c2.63086 11.417 3.96484 23.1553 3.96484 34.8857z" />
<glyph glyph-name="hand-scissors" unicode="&#xf257;"
d="M256 -32c-44.9561 0 -77.3428 43.2627 -64.0244 85.8535c-21.6484 13.71 -34.0156 38.7617 -30.3408 65.0068h-87.6348c-40.8037 0 -74 32.8105 -74 73.1406c0 40.3291 33.1963 73.1396 74 73.1396l94 -9.14062l-78.8496 18.6787
c-38.3076 14.7422 -57.04 57.4707 -41.9424 95.1123c15.0303 37.4736 57.7549 55.7803 95.6416 41.2012l144.929 -55.7568c24.9551 30.5566 57.8086 43.9932 92.2178 24.7324l97.999 -54.8525c20.9746 -11.7393 34.0049 -33.8457 34.0049 -57.6904v-205.702
c0 -30.7422 -21.4404 -57.5576 -51.7979 -64.5537l-118.999 -27.4268c-4.97168 -1.14648 -10.0889 -1.72949 -15.2031 -1.72949zM256 16.0127l70 -0.000976562c1.23633 0 3.21777 0.225586 4.42285 0.501953l119.001 27.4277
c8.58203 1.97754 14.5762 9.29102 14.5762 17.7812v205.701c0 6.4873 -3.62109 12.542 -9.44922 15.8047l-98 54.8545c-8.13965 4.55566 -18.668 2.61914 -24.4873 -4.50781l-21.7646 -26.6475c-2.65039 -3.24512 -8.20215 -5.87891 -12.3926 -5.87891
c-1.64062 0 -4.21484 0.477539 -5.74609 1.06738l-166.549 64.0908c-32.6543 12.5664 -50.7744 -34.5771 -19.2227 -46.7168l155.357 -59.7852c5.66016 -2.17773 10.2539 -8.86816 10.2539 -14.9326v0v-11.6328c0 -8.83691 -7.16309 -16 -16 -16h-182
c-34.375 0 -34.4297 -50.2803 0 -50.2803h182c8.83691 0 16 -7.16309 16 -16v-6.85645c0 -8.83691 -7.16309 -16 -16 -16h-28c-25.1221 0 -25.1592 -36.5674 0 -36.5674h28c8.83691 0 16 -7.16211 16 -16v-6.85547c0 -8.83691 -7.16309 -16 -16 -16
c-25.1201 0 -25.1602 -36.5674 0 -36.5674z" />
<glyph glyph-name="hand-lizard" unicode="&#xf258;" horiz-adv-x="576"
d="M556.686 157.458c12.6357 -19.4863 19.3145 -42.0615 19.3145 -65.2871v-124.171h-224v71.582l-99.751 38.7871c-2.7832 1.08203 -5.70996 1.63086 -8.69727 1.63086h-131.552c-30.8789 0 -56 25.1211 -56 56c0 48.5234 39.4766 88 88 88h113.709l18.333 48h-196.042
c-44.1123 0 -80 35.8877 -80 80v8c0 30.8779 25.1211 56 56 56h293.917c24.5 0 47.084 -12.2725 60.4111 -32.8291zM528 16v76.1709v0.0478516c0 11.7461 -5.19141 29.2734 -11.5879 39.124l-146.358 225.715c-4.44336 6.85254 -11.9707 10.9424 -20.1367 10.9424h-293.917
c-4.41113 0 -8 -3.58887 -8 -8v-8c0 -17.6445 14.3555 -32 32 -32h213.471c25.2021 0 42.626 -25.293 33.6299 -48.8457l-24.5518 -64.2812c-7.05371 -18.4658 -25.0732 -30.873 -44.8398 -30.873h-113.709c-22.0557 0 -40 -17.9443 -40 -40c0 -4.41113 3.58887 -8 8 -8
h131.552h0.0517578c7.44141 0 19.1074 -2.19238 26.041 -4.89355l99.752 -38.7881c18.5898 -7.22852 30.6035 -24.7881 30.6035 -44.7363v-23.582h128z" />
<glyph glyph-name="hand-spock" unicode="&#xf259;"
d="M501.03 331.824c6.05762 -9.77832 10.9746 -27.0498 10.9746 -38.5518c0 -4.80664 -0.915039 -12.499 -2.04297 -17.1709l-57.623 -241.963c-12.748 -54.1729 -68.2627 -98.1387 -123.915 -98.1387h-0.345703h-107.455h-0.224609
c-33.8135 0 -81.2148 18.834 -105.807 42.041l-91.3652 85.9766c-12.8213 12.0469 -23.2266 36.1016 -23.2266 53.6943c0 16.1299 8.97266 38.7529 20.0273 50.499c5.31836 5.66406 29.875 29.3926 68.1152 21.8477l-24.3594 82.1973
c-1.68164 5.66406 -3.0459 15.0576 -3.0459 20.9668c0 37.5938 30.417 70.502 67.8955 73.4551c-0.204102 2.03125 -0.369141 5.33691 -0.369141 7.37891c0 31.627 24.8594 63.6895 55.4902 71.5684c43.248 10.9785 80.5645 -17.7012 89.6602 -53.0723l13.6836 -53.207
l4.64648 22.6602c6.76074 32.417 39.123 58.8115 72.2373 58.916c8.73438 0 56.625 -3.26953 70.7383 -54.0801c15.0664 0.710938 46.9199 -3.50977 66.3105 -35.0176zM463.271 287.219c7.86914 32.9844 -42.1211 45.2695 -50.0859 11.9219l-24.8008 -104.146
c-4.38867 -18.4141 -31.7783 -11.8926 -28.0557 6.2168l28.5479 139.166c7.39844 36.0703 -43.3076 45.0703 -50.1182 11.9629l-31.791 -154.971c-3.54883 -17.3086 -28.2832 -18.0469 -32.7109 -0.804688l-47.3262 184.035
c-8.43359 32.8105 -58.3691 20.2676 -49.8652 -12.8359l42.4414 -165.039c4.81641 -18.7207 -23.3711 -26.9121 -28.9648 -8.00781l-31.3438 105.779c-9.6875 32.6465 -59.1191 18.2578 -49.3867 -14.625l36.0137 -121.539
c5.61816 -18.9521 10.1777 -50.377 10.1777 -70.1436v-0.00878906c0 -6.54297 -8.05664 -10.9355 -13.4824 -5.82617l-51.123 48.1074c-24.7852 23.4082 -60.0527 -14.1875 -35.2793 -37.4902l91.3691 -85.9805c16.9629 -16.0068 49.6592 -28.998 72.9824 -28.998h0.154297
h107.455h0.216797c34.7402 0 69.3936 27.4443 77.3525 61.2598z" />
<glyph glyph-name="hand-pointer" unicode="&#xf25a;" horiz-adv-x="448"
d="M358.182 268.639c43.1934 16.6348 89.8184 -15.7949 89.8184 -62.6387v-84c-0.000976562 -4.25 -0.775391 -11.0615 -1.72754 -15.2041l-27.4297 -118.999c-6.98242 -30.2969 -33.7549 -51.7969 -64.5566 -51.7969h-178.286c-21.2588 0 -41.3682 10.4102 -53.791 27.8457
l-109.699 154.001c-21.2432 29.8193 -14.8047 71.3574 14.5498 93.1523c18.8115 13.9658 42.1748 16.2822 62.083 8.87207v161.129c0 36.9443 29.7363 67 66.2861 67s66.2861 -30.0557 66.2861 -67v-73.6338c20.4131 2.85742 41.4678 -3.94238 56.5947 -19.6289
c27.1934 12.8467 60.3799 5.66992 79.8721 -19.0986zM80.9854 168.303c-14.4004 20.2119 -43.8008 -2.38281 -29.3945 -22.6055l109.712 -154c3.43457 -4.81934 8.92871 -7.69727 14.6973 -7.69727h178.285c8.49219 0 15.8037 5.99414 17.7822 14.5762l27.4297 119.001
c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285v84c0 25.1602 -36.5713 25.1211 -36.5713 0c0 -8.83594 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16406 -16 16v21c0 25.1602 -36.5713 25.1201 -36.5713 0v-21c0 -8.83594 -7.16309 -16 -16 -16h-6.85938
c-8.83691 0 -16 7.16406 -16 16v35c0 25.1602 -36.5703 25.1201 -36.5703 0v-35c0 -8.83594 -7.16309 -16 -16 -16h-6.85742c-8.83691 0 -16 7.16406 -16 16v175c0 25.1602 -36.5713 25.1201 -36.5713 0v-241.493c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832z
M176.143 48v96c0 8.83691 6.26855 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16zM251.571 48v96c0 8.83691 6.26758 16 14 16h6c7.73145 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26855 -16 -14 -16h-6
c-7.73242 0 -14 7.16309 -14 16zM327 48v96c0 8.83691 6.26758 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16z" />
<glyph glyph-name="hand-peace" unicode="&#xf25b;" horiz-adv-x="448"
d="M362.146 256.024c42.5908 13.3184 85.8535 -19.0684 85.8535 -64.0244l-0.0117188 -70.001c-0.000976562 -4.25 -0.775391 -11.0615 -1.72949 -15.2031l-27.4268 -118.999c-6.99707 -30.3564 -33.8105 -51.7969 -64.5547 -51.7969h-205.702
c-23.8447 0 -45.9502 13.0303 -57.6904 34.0059l-54.8525 97.999c-19.2607 34.4092 -5.82422 67.2617 24.7324 92.2178l-55.7568 144.928c-14.5791 37.8867 3.72754 80.6113 41.2012 95.6416c37.6406 15.0977 80.3691 -3.63477 95.1123 -41.9424l18.6787 -78.8496
l-9.14062 94c0 40.8037 32.8096 74 73.1396 74s73.1406 -33.1963 73.1406 -74v-87.6348c26.2451 3.6748 51.2959 -8.69238 65.0068 -30.3408zM399.987 122l-0.000976562 70c0 25.1602 -36.5674 25.1201 -36.5674 0c0 -8.83691 -7.16309 -16 -16 -16h-6.85547
c-8.83789 0 -16 7.16309 -16 16v28c0 25.1592 -36.5674 25.1221 -36.5674 0v-28c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v182c0 34.4297 -50.2803 34.375 -50.2803 0v-182c0 -8.83691 -7.16309 -16 -16 -16h-11.6328v0
c-6.06445 0 -12.7549 4.59375 -14.9326 10.2539l-59.7842 155.357c-12.1396 31.5518 -59.2842 13.4326 -46.7168 -19.2227l64.0898 -166.549c0.589844 -1.53125 1.06738 -4.10547 1.06738 -5.74609c0 -4.19043 -2.63379 -9.74219 -5.87891 -12.3926l-26.6475 -21.7646
c-7.12695 -5.81934 -9.06445 -16.3467 -4.50781 -24.4873l54.8535 -98c3.26367 -5.82812 9.31934 -9.44922 15.8057 -9.44922h205.701c8.49121 0 15.8037 5.99414 17.7812 14.5762l27.4277 119.001c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285z" />
<glyph glyph-name="registered" unicode="&#xf25d;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
M366.442 73.791c4.40332 -7.99219 -1.37012 -17.791 -10.5107 -17.791h-42.8096h-0.0126953c-3.97559 0 -8.71582 2.84961 -10.5801 6.36035l-47.5156 89.3027h-31.958v-83.6631c0 -6.61719 -5.38281 -12 -12 -12h-38.5674c-6.61719 0 -12 5.38281 -12 12v248.304
c0 6.61719 5.38281 12 12 12h78.667c71.251 0 101.498 -32.749 101.498 -85.252c0 -31.6123 -15.2148 -59.2969 -39.4824 -73.1758c3.02148 -4.61719 0.225586 0.199219 53.2715 -96.085zM256.933 208.094c20.9131 0 32.4307 11.5186 32.4316 32.4316
c0 19.5752 -6.5127 31.709 -38.9297 31.709h-27.377v-64.1406h33.875z" />
<glyph glyph-name="calendar-plus" unicode="&#xf271;" horiz-adv-x="448"
d="M336 156v-24c0 -6.59961 -5.40039 -12 -12 -12h-76v-76c0 -6.59961 -5.40039 -12 -12 -12h-24c-6.59961 0 -12 5.40039 -12 12v76h-76c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h76v76c0 6.59961 5.40039 12 12 12h24c6.59961 0 12 -5.40039 12 -12
v-76h76c6.59961 0 12 -5.40039 12 -12zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-minus" unicode="&#xf272;" horiz-adv-x="448"
d="M124 120c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h200c6.59961 0 12 -5.40039 12 -12v-24c0 -6.59961 -5.40039 -12 -12 -12h-200zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-times" unicode="&#xf273;" horiz-adv-x="448"
d="M311.7 73.2998l-17 -17c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-53.7002 53.7998l-53.7002 -53.6992c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-17 17c-4.7002 4.69922 -4.7002 12.2998 0 17l53.7002 53.6992l-53.7002 53.7002c-4.7002 4.7002 -4.7002 12.2998 0 17
l17 17c4.7002 4.7002 12.2998 4.7002 17 0l53.7002 -53.7002l53.7002 53.7002c4.7002 4.7002 12.2998 4.7002 17 0l17 -17c4.7002 -4.7002 4.7002 -12.2998 0 -17l-53.7998 -53.7998l53.6992 -53.7002c4.80078 -4.7002 4.80078 -12.2998 0.100586 -17zM448 336v-352
c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10
v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-check" unicode="&#xf274;" horiz-adv-x="448"
d="M400 384c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h48v52c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-52h128v52c0 6.62695 5.37305 12 12 12h40
c6.62695 0 12 -5.37305 12 -12v-52h48zM394 -16c3.31152 0 6 2.68848 6 6v298h-352v-298c0 -3.31152 2.68848 -6 6 -6h340zM341.151 184.65l-142.31 -141.169c-4.70508 -4.66699 -12.3027 -4.6377 -16.9707 0.0673828l-75.0908 75.6992
c-4.66699 4.70508 -4.6377 12.3027 0.0673828 16.9707l22.7197 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l44.1035 -44.4609l111.072 110.182c4.70508 4.66699 12.3027 4.63672 16.9707 -0.0683594l22.5361 -22.7178
c4.66699 -4.70508 4.63672 -12.3027 -0.0683594 -16.9697z" />
<glyph glyph-name="map" unicode="&#xf279;" horiz-adv-x="576"
d="M560.02 416c8.4502 0 15.9805 -6.83008 15.9805 -16.0195v-346.32c0 -11.9609 -9.01367 -25.2705 -20.1201 -29.71l-151.83 -52.8105c-5.32617 -1.7334 -14.1953 -3.13965 -19.7969 -3.13965c-5.7373 0 -14.8105 1.47363 -20.2529 3.29004l-172 60.71l-170.05 -62.8398
c-1.99023 -0.790039 -4 -1.16016 -5.95996 -1.16016c-8.45996 0 -15.9902 6.83008 -15.9902 16.0195v346.32c0.00292969 11.959 9.0166 25.2686 20.1201 29.71l151.83 52.8105c6.43945 2.08984 13.1201 3.13965 19.8096 3.13965
c5.73242 -0.00195312 14.8008 -1.47168 20.2402 -3.28027l172 -60.7197h0.00976562l170.05 62.8398c1.98047 0.790039 4 1.16016 5.95996 1.16016zM224 357.58v-285.97l128 -45.1904v285.97zM48 29.9502l127.36 47.0801l0.639648 0.229492v286.2l-128 -44.5303v-288.979z
M528 65.0801v288.97l-127.36 -47.0693l-0.639648 -0.240234v-286.19z" />
<glyph glyph-name="comment-alt" unicode="&#xf27a;"
d="M448 448c35.2998 0 64 -28.7002 64 -64v-288c0 -35.2998 -28.7002 -64 -64 -64h-144l-124.9 -93.5996c-2.19922 -1.7002 -4.69922 -2.40039 -7.09961 -2.40039c-6.2002 0 -12 4.90039 -12 12v84h-96c-35.2998 0 -64 28.7002 -64 64v288c0 35.2998 28.7002 64 64 64h384z
M464 96v288c0 8.7998 -7.2002 16 -16 16h-384c-8.7998 0 -16 -7.2002 -16 -16v-288c0 -8.7998 7.2002 -16 16 -16h144v-60l67.2002 50.4004l12.7998 9.59961h160c8.7998 0 16 7.2002 16 16z" />
<glyph glyph-name="pause-circle" unicode="&#xf28b;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16zM240 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16z" />
<glyph glyph-name="stop-circle" unicode="&#xf28d;"
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-160
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h160c8.7998 0 16 -7.2002 16 -16z" />
<glyph glyph-name="handshake" unicode="&#xf2b5;" horiz-adv-x="640"
d="M519.2 320.1h120.8v-255.699h-64c-17.5 0 -31.7998 14.1992 -31.9004 31.6992h-57.8994c-1.7998 -8.19922 -5.2998 -16.0996 -10.9004 -23l-26.2002 -32.2998c-15.7998 -19.3994 -41.8994 -25.5 -64 -16.7998c-13.5 -16.5996 -30.5996 -24 -48.7998 -24
c-15.0996 0 -28.5996 5.09961 -41.0996 15.9004c-31.7998 -21.9004 -74.7002 -21.3008 -105.601 3.7998l-84.5996 76.3994h-9.09961c-0.100586 -17.5 -14.3008 -31.6992 -31.9004 -31.6992h-64v255.699h118l47.5996 47.6006c10.5 10.3994 24.8008 16.2998 39.6006 16.2998
h226.8v0c12.7812 0 30.5225 -7.30273 39.5996 -16.2998zM48 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16zM438 103.3c2.7002 3.40039 2.2002 8.5 -1.2002 11.2998l-108.2 87.8008l-8.19922 -7.5
c-40.3008 -36.8008 -86.7002 -11.8008 -101.5 4.39941c-26.7002 29 -25 74.4004 4.39941 101.3l38.7002 35.5h-56.7002c-2 -0.799805 -3.7002 -1.5 -5.7002 -2.2998l-61.6992 -61.5996h-41.9004v-128.101h27.7002l97.2998 -88
c16.0996 -13.0996 41.4004 -10.5 55.2998 6.60059l15.6006 19.2002l36.7998 -31.5c3 -2.40039 12 -4.90039 18 2.39941l30 36.5l23.8994 -19.3994c3.5 -2.80078 8.5 -2.2002 11.3008 1.19922zM544 144.1v128h-44.7002l-61.7002 61.6006
c-1.39941 1.5 -3.39941 2.2998 -5.5 2.2998l-83.6992 -0.200195c-10 0 -19.6006 -3.7002 -27 -10.5l-65.6006 -60.0996c-9.7002 -8.7998 -10.5 -24 -1.2002 -33.9004c8.90039 -9.39941 25.1006 -8.7002 34.6006 0l55.2002 50.6006c6.5 5.89941 16.5996 5.5 22.5996 -1
l10.9004 -11.7002c6 -6.5 5.5 -16.6006 -1 -22.6006l-12.5 -11.3994l102.699 -83.4004c2.80078 -2.2998 5.40039 -4.89941 7.7002 -7.7002h69.2002zM592 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16z
" />
<glyph glyph-name="envelope-open" unicode="&#xf2b6;"
d="M494.586 283.484c9.6123 -7.94824 17.4141 -24.5205 17.4141 -36.9932v-262.491c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v262.515c0 12.5166 7.84668 29.1279 17.5146 37.0771c4.08008 3.35449 110.688 89.0996 135.15 108.549
c22.6992 18.1426 60.1299 55.8594 103.335 55.8594c43.4365 0 81.2314 -38.1914 103.335 -55.8594c23.5283 -18.707 130.554 -104.773 135.251 -108.656zM464 -10v253.632v0.00488281c0 1.5791 -0.996094 3.66602 -2.22363 4.6582
c-15.8633 12.8232 -108.793 87.5752 -132.366 106.316c-17.5527 14.0195 -49.7168 45.3887 -73.4102 45.3887c-23.6016 0 -55.2451 -30.8799 -73.4102 -45.3887c-23.5713 -18.7393 -116.494 -93.4795 -132.364 -106.293
c-1.40918 -1.13965 -2.22559 -2.85254 -2.22559 -4.66504v-253.653c0 -3.31152 2.68848 -6 6 -6h404c3.31152 0 6 2.68848 6 6zM432.009 177.704c4.24902 -5.15918 3.46484 -12.7949 -1.74512 -16.9814c-28.9746 -23.2822 -59.2734 -47.5967 -70.9287 -56.8623
c-22.6992 -18.1436 -60.1299 -55.8604 -103.335 -55.8604c-43.4521 0 -81.2871 38.2373 -103.335 55.8604c-11.2793 8.9668 -41.7441 33.4131 -70.9268 56.8643c-5.20996 4.1875 -5.99316 11.8223 -1.74512 16.9814l15.2578 18.5283
c4.17773 5.07227 11.6572 5.84277 16.7793 1.72559c28.6182 -23.001 58.5654 -47.0352 70.5596 -56.5713c17.5527 -14.0195 49.7168 -45.3887 73.4102 -45.3887c23.6016 0 55.2461 30.8799 73.4102 45.3887c11.9941 9.53516 41.9434 33.5703 70.5625 56.5684
c5.12207 4.11621 12.6016 3.3457 16.7783 -1.72656z" />
<glyph glyph-name="address-book" unicode="&#xf2b9;" horiz-adv-x="448"
d="M436 288h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48
h320c26.5 0 48 -21.5 48 -48v-48h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM368 -16v416h-320v-416h320zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM118.4 64
c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002
h-179.199z" />
<glyph glyph-name="address-card" unicode="&#xf2bb;" horiz-adv-x="576"
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v352h-480v-352h480zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z
M118.4 64c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002
c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199zM360 128c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 192c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 256c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112z" />
<glyph glyph-name="user-circle" unicode="&#xf2bd;" horiz-adv-x="496"
d="M248 344c53 0 96 -43 96 -96s-43 -96 -96 -96s-96 43 -96 96s43 96 96 96zM248 200c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
c49.7002 0 95.0996 18.2998 130.1 48.4004c-14.8994 23 -40.3994 38.5 -69.5996 39.5c-20.7998 -6.5 -40.5996 -9.60059 -60.5 -9.60059s-39.7002 3.2002 -60.5 9.60059c-29.2002 -0.900391 -54.7002 -16.5 -69.5996 -39.5c35 -30.1006 80.3994 -48.4004 130.1 -48.4004z
M410.7 76.0996c23.3994 32.7002 37.2998 72.7002 37.2998 115.9c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -43.2002 13.9004 -83.2002 37.2998 -115.9c24.5 31.4004 62.2002 51.9004 105.101 51.9004c10.1992 0 26.0996 -9.59961 57.5996 -9.59961
c31.5996 0 47.4004 9.59961 57.5996 9.59961c43 0 80.7002 -20.5 105.101 -51.9004z" />
<glyph glyph-name="id-badge" unicode="&#xf2c1;" horiz-adv-x="384"
d="M336 448c26.5 0 48 -21.5 48 -48v-416c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h288zM336 -16v416h-288v-416h288zM144 336c-8.7998 0 -16 7.2002 -16 16s7.2002 16 16 16h96c8.7998 0 16 -7.2002 16 -16s-7.2002 -16 -16 -16
h-96zM192 160c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM102.4 32c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8
c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199z" />
<glyph glyph-name="id-card" unicode="&#xf2c2;" horiz-adv-x="576"
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v288h-480v-288h32.7998c-1 4.5 -0.799805 -3.59961 -0.799805 22.4004c0 31.7998 30.0996 57.5996 67.2002 57.5996
c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996c0 -26 0.0996094 -17.9004 -0.799805 -22.4004h224.8zM360 96c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16
c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 160c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 224c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM192 128c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z" />
<glyph glyph-name="window-maximize" unicode="&#xf2d0;"
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v234h-416v-234c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="window-minimize" unicode="&#xf2d1;"
d="M480 -32h-448c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32h448c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
<glyph glyph-name="window-restore" unicode="&#xf2d2;"
d="M464 448c26.5 0 48 -21.5 48 -48v-320c0 -26.5 -21.5 -48 -48 -48h-48v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v320c0 26.5 21.5 48 48 48h48v48c0 26.5 21.5 48 48 48h320zM368 -16v208h-320v-208h320zM464 80v320h-320v-48h224
c26.5 0 48 -21.5 48 -48v-224h48z" />
<glyph glyph-name="snowflake" unicode="&#xf2dc;" horiz-adv-x="448"
d="M440.1 92.7998c7.60059 -4.39941 10.1006 -14.2002 5.5 -21.7002l-7.89941 -13.8994c-4.40039 -7.7002 -14 -10.2998 -21.5 -5.90039l-39.2002 23l9.09961 -34.7002c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.5996l-15.2002 -4.09961
c-8.39941 -2.30078 -17.0996 2.7998 -19.2998 11.2998l-21.2998 81l-71.9004 42.2002v-84.5l58.2998 -59.3008c6.10059 -6.19922 6.10059 -16.3994 0 -22.5996l-11.0996 -11.2998c-6.09961 -6.2002 -16.0996 -6.2002 -22.2002 0l-24.8994 25.3994v-46.0996
c0 -8.7998 -7 -16 -15.7002 -16h-15.7002c-8.7002 0 -15.7002 7.2002 -15.7002 16v45.9004l-24.8994 -25.4004c-6.10059 -6.2002 -16.1006 -6.2002 -22.2002 0l-11.1006 11.2998c-6.09961 6.2002 -6.09961 16.4004 0 22.6006l58.3008 59.2998v84.5l-71.9004 -42.2002
l-21.2998 -81c-2.2998 -8.5 -10.9004 -13.5996 -19.2998 -11.2998l-15.2002 4.09961c-8.40039 2.2998 -13.2998 11.1006 -11.1006 19.6006l9.10059 34.6992l-39.2002 -23c-7.5 -4.39941 -17.2002 -1.7998 -21.5 5.90039l-7.90039 13.9004
c-4.2998 7.69922 -1.69922 17.5 5.80078 21.8994l39.1992 23l-34.0996 9.2998c-8.40039 2.30078 -13.2998 11.1006 -11.0996 19.6006l4.09961 15.5c2.2998 8.5 10.9004 13.5996 19.2998 11.2998l79.7002 -21.7002l71.9004 42.2002l-71.9004 42.2002l-79.7002 -21.7002
c-8.39941 -2.2998 -17.0996 2.7998 -19.2998 11.2998l-4.09961 15.5c-2.30078 8.5 2.69922 17.2998 11.0996 19.6006l34.0996 9.09961l-39.1992 23c-7.60059 4.5 -10.1006 14.2002 -5.80078 21.9004l7.90039 13.8994c4.40039 7.7002 14 10.2998 21.5 5.90039l39.2002 -23
l-9.10059 34.7002c-2.2998 8.5 2.7002 17.2998 11.1006 19.5996l15.2002 4.09961c8.39941 2.30078 17.0996 -2.7998 19.2998 -11.2998l21.2998 -81l71.9004 -42.2002v84.5l-58.3008 59.3008c-6.09961 6.19922 -6.09961 16.3994 0 22.5996l11.5 11.2998
c6.10059 6.2002 16.1006 6.2002 22.2002 0l24.9004 -25.3994v46.0996c0 8.7998 7 16 15.7002 16h15.6992c8.7002 0 15.7002 -7.2002 15.7002 -16v-45.9004l24.9004 25.4004c6.09961 6.2002 16.0996 6.2002 22.2002 0l11.0996 -11.2998
c6.09961 -6.2002 6.09961 -16.4004 0 -22.6006l-58.2998 -59.2998v-84.5l71.8994 42.2002l21.3008 81c2.2998 8.5 10.8994 13.5996 19.2998 11.2998l15.2002 -4.09961c8.39941 -2.2998 13.2998 -11.1006 11.0996 -19.6006l-9.09961 -34.6992l39.1992 23
c7.5 4.39941 17.2002 1.7998 21.5 -5.90039l7.90039 -13.9004c4.2998 -7.69922 1.7002 -17.5 -5.7998 -21.8994l-39.2002 -23l34.0996 -9.2998c8.40039 -2.30078 13.3008 -11.1006 11.1006 -19.6006l-4.10059 -15.5c-2.2998 -8.5 -10.8994 -13.5996 -19.2998 -11.2998
l-79.7002 21.7002l-71.8994 -42.2002l71.7998 -42.2002l79.7002 21.7002c8.39941 2.2998 17.0996 -2.7998 19.2998 -11.2998l4.09961 -15.5c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.6006l-34.0996 -9.2998z" />
<glyph glyph-name="trash-alt" unicode="&#xf2ed;" horiz-adv-x="448"
d="M268 32c-6.62402 0 -12 5.37598 -12 12v216c0 6.62402 5.37598 12 12 12h24c6.62402 0 12 -5.37598 12 -12v-216c0 -6.62402 -5.37598 -12 -12 -12h-24zM432 368c8.83203 0 16 -7.16797 16 -16v-16c0 -8.83203 -7.16797 -16 -16 -16h-16v-336
c0 -26.4961 -21.5039 -48 -48 -48h-288c-26.4961 0 -48 21.5039 -48 48v336h-16c-8.83203 0 -16 7.16797 -16 16v16c0 8.83203 7.16797 16 16 16h82.4102l34.0195 56.7002c7.71875 12.8613 26.1572 23.2998 41.1572 23.2998h0.00292969h100.82h0.0224609
c15 0 33.4385 -10.4385 41.1572 -23.2998l34 -56.7002h82.4102zM171.84 397.09l-17.4502 -29.0898h139.221l-17.46 29.0898c-0.96582 1.60645 -3.26953 2.91016 -5.14355 2.91016h-0.00683594h-94h-0.0166016c-1.87402 0 -4.17871 -1.30371 -5.14355 -2.91016zM368 -16v336
h-288v-336h288zM156 32c-6.62402 0 -12 5.37598 -12 12v216c0 6.62402 5.37598 12 12 12h24c6.62402 0 12 -5.37598 12 -12v-216c0 -6.62402 -5.37598 -12 -12 -12h-24z" />
<glyph glyph-name="images" unicode="&#xf302;" horiz-adv-x="576"
d="M480 32v-16c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256c0 26.5098 21.4902 48 48 48h16v-48h-10c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v10h48zM522 368h-372
c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v244c0 3.31152 -2.68848 6 -6 6zM528 416c26.5098 0 48 -21.4902 48 -48v-256c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256
c0 26.5098 21.4902 48 48 48h384zM264 304c0 -22.0908 -17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40s40 -17.9092 40 -40zM192 208l39.5146 39.5146c4.68652 4.68652 12.2842 4.68652 16.9717 0l39.5137 -39.5146l103.515 103.515
c4.68652 4.68652 12.2842 4.68652 16.9717 0l71.5137 -71.5146v-80h-288v48z" />
<glyph glyph-name="clipboard" unicode="&#xf328;" horiz-adv-x="384"
d="M336 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h80c0 35.2998 28.7002 64 64 64s64 -28.7002 64 -64h80zM192 408c-13.2998 0 -24 -10.7002 -24 -24s10.7002 -24 24 -24s24 10.7002 24 24
s-10.7002 24 -24 24zM336 -10v340c0 3.2998 -2.7002 6 -6 6h-42v-36c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v36h-42c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h276c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="arrow-alt-circle-down" unicode="&#xf358;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM224 308c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-116
h67c10.7002 0 16.0996 -12.9004 8.5 -20.5l-99 -99c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-99 99c-7.5 7.59961 -2.2002 20.5 8.5 20.5h67v116z" />
<glyph glyph-name="arrow-alt-circle-left" unicode="&#xf359;"
d="M8 192c0 137 111 248 248 248s248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248zM456 192c0 110.5 -89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200zM384 212v-40c0 -6.59961 -5.40039 -12 -12 -12h-116v-67
c0 -10.7002 -12.9004 -16 -20.5 -8.5l-99 99c-4.7002 4.7002 -4.7002 12.2998 0 17l99 99c7.59961 7.59961 20.5 2.2002 20.5 -8.5v-67h116c6.59961 0 12 -5.40039 12 -12z" />
<glyph glyph-name="arrow-alt-circle-right" unicode="&#xf35a;"
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM128 172v40c0 6.59961 5.40039 12 12 12h116v67
c0 10.7002 12.9004 16 20.5 8.5l99 -99c4.7002 -4.7002 4.7002 -12.2998 0 -17l-99 -99c-7.59961 -7.59961 -20.5 -2.2002 -20.5 8.5v67h-116c-6.59961 0 -12 5.40039 -12 12z" />
<glyph glyph-name="arrow-alt-circle-up" unicode="&#xf35b;"
d="M256 -56c-137 0 -248 111 -248 248s111 248 248 248s248 -111 248 -248s-111 -248 -248 -248zM256 392c-110.5 0 -200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200zM276 64h-40c-6.59961 0 -12 5.40039 -12 12v116h-67
c-10.7002 0 -16 12.9004 -8.5 20.5l99 99c4.7002 4.7002 12.2998 4.7002 17 0l99 -99c7.59961 -7.59961 2.2002 -20.5 -8.5 -20.5h-67v-116c0 -6.59961 -5.40039 -12 -12 -12z" />
<glyph glyph-name="gem" unicode="&#xf3a5;" horiz-adv-x="576"
d="M464 448c4.09961 0 7.7998 -2 10.0996 -5.40039l99.9004 -147.199c2.90039 -4.40039 2.59961 -10.1006 -0.700195 -14.2002l-276 -340.8c-4.7998 -5.90039 -13.7998 -5.90039 -18.5996 0l-276 340.8c-3.2998 4 -3.60059 9.7998 -0.700195 14.2002l100 147.199
c2.2002 3.40039 6 5.40039 10 5.40039h352zM444.7 400h-56.7998l51.6992 -96h68.4004zM242.6 400l-51.5996 -96h194l-51.7002 96h-90.7002zM131.3 400l-63.2998 -96h68.4004l51.6992 96h-56.7998zM88.2998 256l119.7 -160l-68.2998 160h-51.4004zM191.2 256l96.7998 -243.3
l96.7998 243.3h-193.6zM368 96l119.6 160h-51.3994z" />
<glyph glyph-name="money-bill-alt" unicode="&#xf3d1;" horiz-adv-x="640"
d="M320 304c53.0195 0 96 -50.1396 96 -112c0 -61.8701 -43 -112 -96 -112c-53.0195 0 -96 50.1504 -96 112c0 61.8604 42.9805 112 96 112zM360 136v16c0 4.41992 -3.58008 8 -8 8h-16v88c0 4.41992 -3.58008 8 -8 8h-13.5801h-0.000976562
c-4.01074 0 -9.97266 -1.80566 -13.3086 -4.03027l-15.3301 -10.2197c-1.96777 -1.30957 -3.56445 -4.29004 -3.56445 -6.65332c0 -1.33691 0.601562 -3.32422 1.34375 -4.43652l8.88086 -13.3105c1.30859 -1.9668 4.29004 -3.56445 6.65332 -3.56445
c1.33691 0 3.32422 0.602539 4.43652 1.34473l0.469727 0.310547v-55.4404h-16c-4.41992 0 -8 -3.58008 -8 -8v-16c0 -4.41992 3.58008 -8 8 -8h64c4.41992 0 8 3.58008 8 8zM608 384c17.6699 0 32 -14.3301 32 -32v-320c0 -17.6699 -14.3301 -32 -32 -32h-576
c-17.6699 0 -32 14.3301 -32 32v320c0 17.6699 14.3301 32 32 32h576zM592 112v160c-35.3496 0 -64 28.6504 -64 64h-416c0 -35.3496 -28.6504 -64 -64 -64v-160c35.3496 0 64 -28.6504 64 -64h416c0 35.3496 28.6504 64 64 64z" />
<glyph glyph-name="window-close" unicode="&#xf410;"
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v340c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z
M356.5 253.4l-61.4004 -61.4004l61.4004 -61.4004c4.59961 -4.59961 4.59961 -12.0996 0 -16.7998l-22.2998 -22.2998c-4.60059 -4.59961 -12.1006 -4.59961 -16.7998 0l-61.4004 61.4004l-61.4004 -61.4004c-4.59961 -4.59961 -12.0996 -4.59961 -16.7998 0
l-22.2998 22.2998c-4.59961 4.60059 -4.59961 12.1006 0 16.7998l61.4004 61.4004l-61.4004 61.4004c-4.59961 4.59961 -4.59961 12.0996 0 16.7998l22.2998 22.2998c4.60059 4.59961 12.1006 4.59961 16.7998 0l61.4004 -61.4004l61.4004 61.4004
c4.59961 4.59961 12.0996 4.59961 16.7998 0l22.2998 -22.2998c4.7002 -4.60059 4.7002 -12.1006 0 -16.7998z" />
<glyph glyph-name="comment-dots" unicode="&#xf4ad;"
d="M144 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM256 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM368 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32s14.2998 32 32 32zM256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5
c-3.80078 8.7998 -2 19 4.59961 26c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160
c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
<glyph glyph-name="smile-wink" unicode="&#xf4da;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM365.8 138.4c10.2002 -8.5 11.6006 -23.6006 3.10059 -33.8008
c-30 -36 -74.1006 -56.5996 -120.9 -56.5996s-90.9004 20.5996 -120.9 56.5996c-8.39941 10.2002 -7.09961 25.3008 3.10059 33.8008c10.0996 8.39941 25.2998 7.09961 33.7998 -3.10059c20.7998 -25.0996 51.5 -39.3994 84 -39.3994s63.2002 14.3994 84 39.3994
c8.5 10.2002 23.5996 11.6006 33.7998 3.10059zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 268c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008l-9.5 8.5
c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996z" />
<glyph glyph-name="angry" unicode="&#xf556;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 136c33.5996 0 65.2002 -14.7998 86.7998 -40.5996
c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008c-10.6006 -8.89941 -25.7002 -6.69922 -33.7998 3c-24.8008 29.7002 -75 29.7002 -99.8008 0c-8.5 -10.1992 -23.5996 -11.5 -33.7998 -3s-11.5996 23.6006 -3.09961 33.8008
c21.5996 25.7998 53.2002 40.5996 86.7998 40.5996zM200 208c0 -17.7002 -14.2998 -32.0996 -32 -32.0996s-32 14.2998 -32 32c0 6.19922 2.2002 11.6992 5.2998 16.5996l-28.2002 8.5c-12.6992 3.7998 -19.8994 17.2002 -16.0996 29.9004
c3.7998 12.6992 17.0996 20 29.9004 16.0996l80 -24c12.6992 -3.7998 19.8994 -17.2002 16.0996 -29.9004c-3.09961 -10.3994 -12.7002 -17.0996 -23 -17.0996zM399 262.9c3.7998 -12.7002 -3.40039 -26.1006 -16.0996 -29.8008l-28.2002 -8.5
c3.09961 -4.89941 5.2998 -10.3994 5.2998 -16.5996c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32c-10.2998 0 -19.9004 6.7002 -23 17.0996c-3.7998 12.7002 3.40039 26.1006 16.0996 29.9004l80 24c12.8008 3.7998 26.1006 -3.40039 29.9004 -16.0996z" />
<glyph glyph-name="dizzy" unicode="&#xf567;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM214.2 209.9
c-7.90039 -7.90039 -20.5 -7.90039 -28.4004 -0.200195l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998
c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.9004c7.7998 7.7998 20.5 7.7998 28.2998 0s7.7998 -20.5 0 -28.2998l-17.9004 -17.9004l17.9004 -17.7998c7.7998 -7.7998 7.7998 -20.5 0 -28.2998zM374.2 302.1
c7.7002 -7.7998 7.7002 -20.3994 0 -28.1992l-17.9004 -17.9004l17.7998 -18c7.80078 -7.7998 7.80078 -20.5 0 -28.2998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0
c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.7998c7.7998 7.80078 20.5 7.80078 28.2998 0zM248 176
c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64s-64 28.7002 -64 64s28.7002 64 64 64z" />
<glyph glyph-name="flushed" unicode="&#xf579;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM344 304c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80
s-80 35.7998 -80 80s35.7998 80 80 80zM344 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM344 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM232 224c0 -44.2002 -35.7998 -80 -80 -80
s-80 35.7998 -80 80s35.7998 80 80 80s80 -35.7998 80 -80zM152 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM152 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM312 104
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
<glyph glyph-name="frown-open" unicode="&#xf57a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32
s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 160c35.5996 0 88.7998 -21.2998 95.7998 -61.2002c2 -11.7998 -9.09961 -21.5996 -20.5 -18.0996
c-31.2002 9.59961 -59.3994 15.2998 -75.2998 15.2998s-44.0996 -5.7002 -75.2998 -15.2998c-11.5 -3.40039 -22.5 6.2998 -20.5 18.0996c7 39.9004 60.2002 61.2002 95.7998 61.2002z" />
<glyph glyph-name="grimace" unicode="&#xf57f;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM344 192c26.5 0 48 -21.5 48 -48v-32c0 -26.5 -21.5 -48 -48 -48h-192c-26.5 0 -48 21.5 -48 48v32c0 26.5 21.5 48 48 48
h192zM176 96v24h-40v-8c0 -8.7998 7.2002 -16 16 -16h24zM176 136v24h-24c-8.7998 0 -16 -7.2002 -16 -16v-8h40zM240 96v24h-48v-24h48zM240 136v24h-48v-24h48zM304 96v24h-48v-24h48zM304 136v24h-48v-24h48zM360 112v8h-40v-24h24c8.7998 0 16 7.2002 16 16zM360 136v8
c0 8.7998 -7.2002 16 -16 16h-24v-24h40z" />
<glyph glyph-name="grin" unicode="&#xf580;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
<glyph glyph-name="grin-alt" unicode="&#xf581;" horiz-adv-x="496"
d="M200.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56
c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM328.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0
c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200
s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006
s79.7002 4.7998 105.6 13.1006z" />
<glyph glyph-name="grin-beam" unicode="&#xf582;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM117.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996
l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002zM277.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998
c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002z" />
<glyph glyph-name="grin-beam-sweat" unicode="&#xf583;" horiz-adv-x="496"
d="M440 288c-29.5 0 -53.2998 26.2998 -53.2998 58.7002c0 25 31.7002 75.5 46.2002 97.2998c3.5 5.2998 10.5996 5.2998 14.1992 0c14.5 -21.7998 46.2002 -72.2998 46.2002 -97.2998c0 -32.4004 -23.7998 -58.7002 -53.2998 -58.7002zM248 48
c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM378.3 216.3
c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM483.6 269.2c8 -24.2998 12.4004 -50.2002 12.4004 -77.2002c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248
c45.7002 0 88.4004 -12.5996 125.2 -34.2002c-10.9004 -21.5996 -15.5 -36.2002 -17.2002 -45.7002c-31.2002 20.1006 -68.2002 31.9004 -108 31.9004c-110.3 0 -200 -89.7002 -200 -200s89.7002 -200 200 -200s200 89.7002 200 200
c0 22.5 -3.90039 44.0996 -10.7998 64.2998c0.399414 0 21.7998 -2.7998 46.3994 12.9004zM168 258.6c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
<glyph glyph-name="grin-hearts" unicode="&#xf584;" horiz-adv-x="496"
d="M353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM200.8 192.3
l-70.2002 18.1006c-20.3994 5.2998 -31.8994 27 -24.1992 47.1992c6.69922 17.7002 26.6992 26.7002 44.8994 22l7.10059 -1.89941l2 7.09961c5.09961 18.1006 22.8994 30.9004 41.5 27.9004c21.3994 -3.40039 34.3994 -24.2002 28.7998 -44.5l-19.4004 -69.9004
c-1.2998 -4.5 -6 -7.2002 -10.5 -6zM389.6 257.6c7.7002 -20.1992 -3.7998 -41.7998 -24.1992 -47.0996l-70.2002 -18.2002c-4.60059 -1.2002 -9.2998 1.5 -10.5 6l-19.4004 69.9004c-5.59961 20.2998 7.40039 41.0996 28.7998 44.5c18.7002 3 36.5 -9.7998 41.5 -27.9004
l2 -7.09961l7.10059 1.89941c18.2002 4.7002 38.2002 -4.39941 44.8994 -22zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200
s89.7002 -200 200 -200z" />
<glyph glyph-name="grin-squint" unicode="&#xf585;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM118.9 184.2c-3.80078 4.39941 -3.90039 11 -0.100586 15.5l33.6006 40.2998
l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5c3.89941 4.40039 10.1992 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5zM361.8 181.7
l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5.10059 2.90039 11.5 1.90039 15.2998 -2.5c3.80078 -4.5 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5
c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5z" />
<glyph glyph-name="grin-squint-tears" unicode="&#xf586;"
d="M117.1 63.9004c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998
c16.8994 16.9004 75.0996 26.9004 100.899 30.6006zM75.9004 105.6c-19.6006 -3.89941 -35.1006 -8.09961 -47.3008 -12.1992c-39.2998 90.5996 -22.0996 199.899 52 274c48.5 48.3994 111.9 72.5996 175.4 72.5996c38.9004 0 77.7998 -9.2002 113.2 -27.4004
c-4 -12.1992 -8.2002 -28 -12 -48.2998c-30.4004 17.9004 -65 27.7002 -101.2 27.7002c-53.4004 0 -103.6 -20.7998 -141.4 -58.5996c-61.5996 -61.5 -74.2998 -153.4 -38.6992 -227.801zM428.2 293.2c20.2998 3.89941 36.2002 8 48.5 12
c47.8994 -93.2002 32.8994 -210.5 -45.2002 -288.601c-48.5 -48.3994 -111.9 -72.5996 -175.4 -72.5996c-33.6992 0 -67.2998 7 -98.6992 20.5996c4.19922 12.2002 8.2998 27.7002 12.1992 47.2002c26.6006 -12.7998 55.9004 -19.7998 86.4004 -19.7998
c53.4004 0 103.6 20.7998 141.4 58.5996c65.6992 65.7002 75.7998 166 30.7998 242.601zM394.9 320.1c-6.30078 -0.899414 -11.7002 4.5 -10.9004 10.9004c3.7002 25.7998 13.7002 84 30.5996 100.9c22 21.8994 57.9004 21.5 80.3008 -0.900391
c22.3994 -22.4004 22.7998 -58.4004 0.899414 -80.2998c-16.8994 -16.9004 -75.0996 -26.9004 -100.899 -30.6006zM207.9 211.8c3 -3 4.19922 -7.2998 3.19922 -11.5l-22.5996 -90.5c-1.40039 -5.39941 -6.2002 -9.09961 -11.7002 -9.09961h-0.899414
c-5.80078 0.5 -10.5 5.09961 -11 10.8994l-4.80078 52.3008l-52.2998 4.7998c-5.7998 0.5 -10.3994 5.2002 -10.8994 11c-0.400391 5.89941 3.39941 11.2002 9.09961 12.5996l90.5 22.7002c4.2002 1 8.40039 -0.200195 11.4004 -3.2002zM247.6 236.9
c-0.0996094 0 -6.39941 -1.80078 -11.3994 3.19922c-3 3 -4.2002 7.30078 -3.2002 11.4004l22.5996 90.5c1.40039 5.7002 7 9.2002 12.6006 9.09961c5.7998 -0.5 10.5 -5.09961 11 -10.8994l4.7998 -52.2998l52.2998 -4.80078c5.7998 -0.5 10.4004 -5.19922 10.9004 -11
c0.399414 -5.89941 -3.40039 -11.1992 -9.10059 -12.5996zM299.6 148.4c29.1006 29.0996 53 59.5996 65.3008 83.7998c4.89941 9.2998 17.5996 9.89941 23.3994 1.7002c27.7002 -38.9004 6.10059 -106.9 -30.5996 -143.7s-104.8 -58.2998 -143.7 -30.6006
c-8.2998 5.90039 -7.5 18.6006 1.7002 23.4004c24.2002 12.5 54.7998 36.2998 83.8994 65.4004z" />
<glyph glyph-name="grin-stars" unicode="&#xf587;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM125.7 200.9l6.09961 34.8994l-25.3994 24.6006
c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006l34.9004 -5c6.19922 -1 8.7998 -8.69922 4.2998 -13.1992l-25.4004 -24.6006l6 -34.8994c1 -6.2002 -5.39941 -11 -11 -7.90039
l-31.2998 16.2998l-31.2998 -16.2998c-5.60059 -3.09961 -12 1.7002 -11 7.90039zM385.4 273.6c6.19922 -1 8.89941 -8.59961 4.39941 -13.1992l-25.3994 -24.6006l6 -34.8994c1 -6.2002 -5.40039 -11 -11 -7.90039l-31.3008 16.2998l-31.2998 -16.2998
c-5.59961 -3.09961 -12 1.7002 -11 7.90039l6 34.8994l-25.3994 24.6006c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006z" />
<glyph glyph-name="grin-tears" unicode="&#xf588;" horiz-adv-x="640"
d="M117.1 191.9c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998c16.8994 16.9004 75.0996 26.9004 100.899 30.6006
zM623.8 161.3c21.9004 -21.8994 21.5 -57.8994 -0.799805 -80.2002c-22.4004 -22.3994 -58.4004 -22.7998 -80.2998 -0.899414c-16.9004 16.8994 -26.9004 75.0996 -30.6006 100.899c-0.899414 6.30078 4.5 11.7002 10.8008 10.8008
c25.7998 -3.7002 84 -13.7002 100.899 -30.6006zM497.2 99.5996c12.3994 -37.2998 25.0996 -43.7998 28.2998 -46.5c-44.5996 -65.7998 -120 -109.1 -205.5 -109.1s-160.9 43.2998 -205.5 109.1c3.09961 2.60059 15.7998 9.10059 28.2998 46.5
c33.4004 -63.8994 100.3 -107.6 177.2 -107.6s143.8 43.7002 177.2 107.6zM122.7 223.5c-2.40039 0.299805 -5 2.5 -49.5 -6.90039c12.3994 125.4 118.1 223.4 246.8 223.4s234.4 -98 246.8 -223.5c-44.2998 9.40039 -47.3994 7.2002 -49.5 7
c-15.2002 95.2998 -97.7998 168.5 -197.3 168.5s-182.1 -73.2002 -197.3 -168.5zM320 48c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996
c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM450.3 216.3c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM240 258.6
c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
<glyph glyph-name="grin-tongue" unicode="&#xf589;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32
s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="grin-tongue-squint" unicode="&#xf58a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998
l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 3 11.5 1.90039 15.2998 -2.5zM214.2 250.3
c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5
c3.89941 4.5 10.2998 5.5 15.2998 2.5z" />
<glyph glyph-name="grin-tongue-wink" unicode="&#xf58b;" horiz-adv-x="496"
d="M152 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
c-3.7998 -3.39941 -9.2998 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM328 320c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80s-80 35.7998 -80 80s35.7998 80 80 80zM328 192
c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM328 264c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248z
M312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998
c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3
c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998
c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998z" />
<glyph glyph-name="grin-wink" unicode="&#xf58c;" horiz-adv-x="496"
d="M328 268c25.6904 0 55.8799 -16.9199 59.8701 -42.1201c1.72949 -11.0898 -11.3506 -18.2695 -19.8301 -10.8398l-9.5498 8.47949c-14.8105 13.1904 -46.1602 13.1904 -60.9707 0l-9.5498 -8.47949c-8.33008 -7.40039 -21.5801 -0.379883 -19.8301 10.8398
c3.98047 25.2002 34.1699 42.1201 59.8604 42.1201zM168 208c-17.6699 0 -32 14.3301 -32 32s14.3301 32 32 32s32 -14.3301 32 -32s-14.3301 -32 -32 -32zM353.55 143.36c10.04 3.13965 19.3906 -5.4502 17.71 -15.3408
c-7.92969 -47.1494 -71.3193 -80.0195 -123.26 -80.0195s-115.33 32.8701 -123.26 80.0195c-1.69043 9.9707 7.76953 18.4707 17.71 15.3408c25.9297 -8.31055 64.3994 -13.0605 105.55 -13.0605s79.6201 4.75977 105.55 13.0605zM248 440c136.97 0 248 -111.03 248 -248
s-111.03 -248 -248 -248s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
<glyph glyph-name="kiss" unicode="&#xf596;" horiz-adv-x="496"
d="M168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM248 440c137 0 248 -111 248 -248
s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z
" />
<glyph glyph-name="kiss-beam" unicode="&#xf597;" horiz-adv-x="496"
d="M168 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM328 296
c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
<glyph glyph-name="kiss-wink-heart" unicode="&#xf598;" horiz-adv-x="504"
d="M304 139.5c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002
c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM374.5 223c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-2.5 -2.2998 -7.90039 -4.7002 -13.7002 -1.59961c-4.39941 2.39941 -6.89941 7.39941 -6.09961 12.3994
c3.89941 25.2002 34.2002 42.1006 59.7998 42.1006s55.7998 -16.9004 59.7998 -42.1006c0.799805 -5 -1.7002 -10 -6.09961 -12.3994c-4.40039 -2.40039 -9.90039 -1.7002 -13.7002 1.59961zM136 239.5c0 17.7002 14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32zM501.1 45.5c9.2002 -23.9004 -4.39941 -49.4004 -28.5 -55.7002l-83 -21.5c-5.39941 -1.39941 -10.8994 1.7998 -12.3994 7.10059l-22.9004 82.5996c-6.59961 24 8.7998 48.5996 34 52.5996c22 3.5 43.1006 -11.5996 49 -33l2.2998 -8.39941
l8.40039 2.2002c21.5996 5.59961 45.0996 -5.10059 53.0996 -25.9004zM334 11.7002c17.7002 -64 10.9004 -39.5 13.4004 -46.7998c-30.5 -13.4004 -64 -20.9004 -99.4004 -20.9004c-137 0 -248 111 -248 248s111 248 248 248s248 -111 247.9 -248
c0 -31.7998 -6.2002 -62.0996 -17.1006 -90c-6 1.5 -12.2002 2.7998 -18.5996 2.90039c-5.60059 9.69922 -13.6006 17.5 -22.6006 23.8994c6.7002 19.9004 10.4004 41.1006 10.4004 63.2002c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200
c30.7998 0 59.9004 7.2002 86 19.7002z" />
<glyph glyph-name="laugh" unicode="&#xf599;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM168 224
c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-beam" unicode="&#xf59a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.700195 -8.5 -10.7998 -11.8994 -14.8994 -4.5
l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006c-12.3008 0 -23.8008 -7.90039 -31.5 -21.6006l-9.5 -17c-4.10059 -7.39941 -15.6006 -4.09961 -14.9004 4.5c3.2998 42.1006 32.2002 71.4004 56 71.4004zM127 220.1c-4.2002 -7.39941 -15.7002 -4 -15.0996 4.5
c3.2998 42.1006 32.1992 71.4004 56 71.4004c23.7998 0 52.6992 -29.2998 56 -71.4004c0.699219 -8.5 -10.8008 -11.8994 -14.9004 -4.5l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006s-23.7998 -7.90039 -31.5 -21.6006zM362.4 160c8.19922 0 14.5 -7 13.5 -15
c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-squint" unicode="&#xf59b;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM343.6 252l33.6006 -40.2998c8.59961 -10.4004 -3.90039 -24.7998 -15.4004 -18l-80 48
c-7.7998 4.7002 -7.7998 15.8994 0 20.5996l80 48c11.6006 6.7998 24 -7.7002 15.4004 -18zM134.2 193.7c-11.6006 -6.7998 -24.1006 7.59961 -15.4004 18l33.6006 40.2998l-33.6006 40.2998c-8.59961 10.2998 3.7998 24.9004 15.4004 18l80 -48
c7.7998 -4.7002 7.7998 -15.8994 0 -20.5996zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-wink" unicode="&#xf59c;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 284c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008
l-9.5 8.5c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996zM168 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z
M362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="meh-blank" unicode="&#xf5a4;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="meh-rolling-eyes" unicode="&#xf5a5;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM336 296c39.7998 0 72 -32.2002 72 -72s-32.2002 -72 -72 -72
s-72 32.2002 -72 72s32.2002 72 72 72zM336 184c22.0996 0 40 17.9004 40 40c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998
c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998c0 -22.0996 17.9004 -40 40 -40zM232 224c0 -39.7998 -32.2002 -72 -72 -72s-72 32.2002 -72 72s32.2002 72 72 72s72 -32.2002 72 -72zM120 224c0 -22.0996 17.9004 -40 40 -40s40 17.9004 40 40
c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998zM312 96
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
<glyph glyph-name="sad-cry" unicode="&#xf5b3;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM392 53.5996c34.5996 35.9004 56 84.7002 56 138.4c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -53.7002 21.4004 -102.4 56 -138.4v114.4
c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-151.4c28.5 -15.5996 61.2002 -24.5996 96 -24.5996s67.5 9 96 24.5996v151.4c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-114.4zM205.8 213.5c-5.7998 -3.2002 -11.2002 -0.700195 -13.7002 1.59961l-9.5 8.5
c-14.7998 13.2002 -46.1992 13.2002 -61 0l-9.5 -8.5c-3.7998 -3.39941 -9.2998 -4 -13.6992 -1.59961c-4.40039 2.40039 -6.90039 7.40039 -6.10059 12.4004c3.90039 25.1992 34.2002 42.0996 59.7998 42.0996c25.6006 0 55.8008 -16.9004 59.8008 -42.0996
c0.799805 -5 -1.7002 -10 -6.10059 -12.4004zM344 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
c-3.7998 -3.39941 -9.2002 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM248 176c30.9004 0 56 -28.7002 56 -64s-25.0996 -64 -56 -64s-56 28.7002 -56 64s25.0996 64 56 64z" />
<glyph glyph-name="sad-tear" unicode="&#xf5b4;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM256 144c38.0996 0 74 -16.7998 98.5 -46.0996
c8.5 -10.2002 7.09961 -25.3008 -3.09961 -33.8008c-10.6006 -8.7998 -25.7002 -6.69922 -33.8008 3.10059c-15.2998 18.2998 -37.7998 28.7998 -61.5996 28.7998c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM162.4 173.2c2.7998 3.7002 8.39941 3.7002 11.1992 0c11.4004 -15.2998 36.4004 -50.6006 36.4004 -68.1006
c0 -22.6992 -18.7998 -41.0996 -42 -41.0996s-42 18.4004 -42 41.0996c0 17.5 25 52.8008 36.4004 68.1006z" />
<glyph glyph-name="smile-beam" unicode="&#xf5b8;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM332 135.4c8.5 10.1992 23.5996 11.5 33.7998 3.09961
c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.2002 8.40039 25.2998 7.09961 33.7998 -3.09961
c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004zM136.5 237l-9.5 -17c-1.90039 -3.2002 -5.90039 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996zM328 296c23.7998 0 52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002
c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
<glyph glyph-name="surprise" unicode="&#xf5c2;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 168c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64
s-64 28.7002 -64 64s28.7002 64 64 64zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="tired" unicode="&#xf5c8;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5
l-33.6006 -40.2998l33.6006 -40.2998c3.7998 -4.5 3.7002 -11 -0.100586 -15.5c-3.5 -4.10059 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 2.90039 11.5 1.90039 15.2998 -2.5z
M220 240c0 -4.2002 -2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7998 -1.60059 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7998 4.5 -3.7002 11 0.100586 15.5
c3.7998 4.40039 10.2998 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998zM248 176c45.4004 0 100.9 -38.2998 107.8 -93.2998c1.5 -11.9004 -7 -21.6006 -15.5 -17.9004c-22.7002 9.7002 -56.2998 15.2002 -92.2998 15.2002
s-69.5996 -5.5 -92.2998 -15.2002c-8.60059 -3.7002 -17 6.10059 -15.5 17.9004c6.89941 55 62.3994 93.2998 107.8 93.2998z" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
static/js/jquery-1.12.4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

17
static/js/jquery.fancybox.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
/* idTabs ~ Sean Catchpole - Version 2.2 - MIT/GPL */
(function(){var dep={"jQuery":"http://code.jquery.com/jquery-latest.min.js"};var init=function(){(function($){$.fn.idTabs=function(){var s={};for(var i=0;i<arguments.length;++i){var a=arguments[i];switch(a.constructor){case Object:$.extend(s,a);break;case Boolean:s.change=a;break;case Number:s.start=a;break;case Function:s.click=a;break;case String:if(a.charAt(0)=='.')s.selected=a;else if(a.charAt(0)=='!')s.event=a;else s.start=a;break;}}
if(typeof s['return']=="function")
s.change=s['return'];return this.each(function(){$.idTabs(this,s);});}
$.idTabs=function(tabs,options){var meta=($.metadata)?$(tabs).metadata():{};var s=$.extend({},$.idTabs.settings,meta,options);if(s.selected.charAt(0)=='.')s.selected=s.selected.substr(1);if(s.event.charAt(0)=='!')s.event=s.event.substr(1);if(s.start==null)s.start=-1;var showId=function(){if($(this).is('.'+s.selected))
return s.change;var id="#"+this.href.split('#')[1];var aList=[];var idList=[];$("a",tabs).each(function(){if(this.href.match(/#/)){aList.push(this);idList.push("#"+this.href.split('#')[1]);}});if(s.click&&!s.click.apply(this,[id,idList,tabs,s]))return s.change;for(i in aList)$(aList[i]).removeClass(s.selected);for(i in idList)$(idList[i]).hide();$(this).addClass(s.selected);$(id).show();return s.change;}
var list=$("a[href*='#']",tabs).unbind(s.event,showId).bind(s.event,showId);list.each(function(){$("#"+this.href.split('#')[1]).hide();});var test=false;if((test=list.filter('.'+s.selected)).length);else if(typeof s.start=="number"&&(test=list.eq(s.start)).length);else if(typeof s.start=="string"&&(test=list.filter("[href*='#"+s.start+"']")).length);if(test){test.removeClass(s.selected);test.trigger(s.event);}
return s;}
$.idTabs.settings={start:0,change:false,click:null,selected:".selected",event:"!click"};$.idTabs.version="2.2";$(function(){$(".idTabs").idTabs();});})(jQuery);}
var check=function(o,s){s=s.split('.');while(o&&s.length)o=o[s.shift()];return o;}
var head=document.getElementsByTagName("head")[0];var add=function(url){var s=document.createElement("script");s.type="text/javascript";s.src=url;head.appendChild(s);}
var s=document.getElementsByTagName('script');var src=s[s.length-1].src;var ok=true;for(d in dep){if(check(this,d))continue;ok=false;add(dep[d]);}if(ok)return init();add(src);})();

View File

@@ -0,0 +1,33 @@
/*
* Plugin Name: QuickFilter
* Author: Collin Henderson (collin@syropia.net)
* Version: 1.0
* © 2012, http://syropia.net
* You are welcome to freely use and modify this script in your personal and commercial products. Please don't sell it or release it as your own work. Thanks!
*/
(function($){
$.extend($.expr[':'], {missing: function (elem, index, match) {
return (elem.textContent || elem.innerText || "").toLowerCase().indexOf(match[3]) == -1;
}});
$.extend($.expr[':'], {exists: function(elem, i, match, array){
return (elem.textContent || elem.innerText || '').toLowerCase().indexOf((match[3] || "").toLowerCase()) >= 0;
}});
$.extend($.fn,{
quickfilter: function(el){
return this.each(function(){
var _this = $(this);
var query = _this.val().toLowerCase();
_this.keyup(function () {
query = $(this).val().toLowerCase();
if(query.replace(/\s/g,"") != ""){
$(el+':exists("' + query.toString() + '")').show();
$(el+':missing("' + query.toString() + '")').hide();
}
else {
$(el).show();
}
});
});
}
});
})(jQuery);

View File

@@ -0,0 +1,18 @@
/*
jQuery Tools 1.2.5 Tooltip - UI essentials
NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
http://flowplayer.org/tools/tooltip/
Since: November 2008
Date: Wed Sep 22 06:02:10 2010 +0000
*/
(function(f){function p(a,b,c){var h=c.relative?a.position().top:a.offset().top,d=c.relative?a.position().left:a.offset().left,i=c.position[0];h-=b.outerHeight()-c.offset[0];d+=a.outerWidth()+c.offset[1];if(/iPad/i.test(navigator.userAgent))h-=f(window).scrollTop();var j=b.outerHeight()+a.outerHeight();if(i=="center")h+=j/2;if(i=="bottom")h+=j;i=c.position[1];a=b.outerWidth()+a.outerWidth();if(i=="center")d-=a/2;if(i=="left")d-=a;return{top:h,left:d}}function u(a,b){var c=this,h=a.add(c),d,i=0,j=
0,m=a.attr("title"),q=a.attr("data-tooltip"),r=o[b.effect],l,s=a.is(":input"),v=s&&a.is(":checkbox, :radio, select, :button, :submit"),t=a.attr("type"),k=b.events[t]||b.events[s?v?"widget":"input":"def"];if(!r)throw'Nonexistent effect "'+b.effect+'"';k=k.split(/,\s*/);if(k.length!=2)throw"Tooltip: bad events configuration for "+t;a.bind(k[0],function(e){clearTimeout(i);if(b.predelay)j=setTimeout(function(){c.show(e)},b.predelay);else c.show(e)}).bind(k[1],function(e){clearTimeout(j);if(b.delay)i=
setTimeout(function(){c.hide(e)},b.delay);else c.hide(e)});if(m&&b.cancelDefault){a.removeAttr("title");a.data("title",m)}f.extend(c,{show:function(e){if(!d){if(q)d=f(q);else if(b.tip)d=f(b.tip).eq(0);else if(m)d=f(b.layout).addClass(b.tipClass).appendTo(document.body).hide().append(m);else{d=a.next();d.length||(d=a.parent().next())}if(!d.length)throw"Cannot find tooltip for "+a;}if(c.isShown())return c;d.stop(true,true);var g=p(a,d,b);b.tip&&d.html(a.data("title"));e=e||f.Event();e.type="onBeforeShow";
h.trigger(e,[g]);if(e.isDefaultPrevented())return c;g=p(a,d,b);d.css({position:"absolute",top:g.top,left:g.left});l=true;r[0].call(c,function(){e.type="onShow";l="full";h.trigger(e)});g=b.events.tooltip.split(/,\s*/);if(!d.data("__set")){d.bind(g[0],function(){clearTimeout(i);clearTimeout(j)});g[1]&&!a.is("input:not(:checkbox, :radio), textarea")&&d.bind(g[1],function(n){n.relatedTarget!=a[0]&&a.trigger(k[1].split(" ")[0])});d.data("__set",true)}return c},hide:function(e){if(!d||!c.isShown())return c;
e=e||f.Event();e.type="onBeforeHide";h.trigger(e);if(!e.isDefaultPrevented()){l=false;o[b.effect][1].call(c,function(){e.type="onHide";h.trigger(e)});return c}},isShown:function(e){return e?l=="full":l},getConf:function(){return b},getTip:function(){return d},getTrigger:function(){return a}});f.each("onHide,onBeforeShow,onShow,onBeforeHide".split(","),function(e,g){f.isFunction(b[g])&&f(c).bind(g,b[g]);c[g]=function(n){n&&f(c).bind(g,n);return c}})}f.tools=f.tools||{version:"1.2.5"};f.tools.tooltip=
{conf:{effect:"toggle",fadeOutSpeed:"fast",predelay:0,delay:30,opacity:1,tip:0,position:["top","center"],offset:[0,0],relative:false,cancelDefault:true,events:{def:"mouseenter,mouseleave",input:"focus,blur",widget:"focus mouseenter,blur mouseleave",tooltip:"mouseenter,mouseleave"},layout:"<div/>",tipClass:"tooltip"},addEffect:function(a,b,c){o[a]=[b,c]}};var o={toggle:[function(a){var b=this.getConf(),c=this.getTip();b=b.opacity;b<1&&c.css({opacity:b});c.show();a.call()},function(a){this.getTip().hide();
a.call()}],fade:[function(a){var b=this.getConf();this.getTip().fadeTo(b.fadeInSpeed,b.opacity,a)},function(a){this.getTip().fadeOut(this.getConf().fadeOutSpeed,a)}]};f.fn.tooltip=function(a){var b=this.data("tooltip");if(b)return b;a=f.extend(true,{},f.tools.tooltip.conf,a);if(typeof a.position=="string")a.position=a.position.split(/,?\s/);this.each(function(){b=new u(f(this),a);f(this).data("tooltip",b)});return a.api?b:this}})(jQuery);

7
static/js/stupidtable.min.js vendored Normal file
View File

@@ -0,0 +1,7 @@
(function(c){c.fn.stupidtable=function(a){return this.each(function(){var b=c(this);a=a||{};a=c.extend({},c.fn.stupidtable.default_sort_fns,a);b.data("sortFns",a);b.stupidtable_build();b.on("click.stupidtable","thead th",function(){c(this).stupidsort()});b.find("th[data-sort-onload=yes]").eq(0).stupidsort()})};c.fn.stupidtable.default_settings={should_redraw:function(a){return!0},will_manually_build_table:!1};c.fn.stupidtable.dir={ASC:"asc",DESC:"desc"};c.fn.stupidtable.default_sort_fns={"int":function(a,
b){return parseInt(a,10)-parseInt(b,10)},"float":function(a,b){return parseFloat(a)-parseFloat(b)},string:function(a,b){return a.toString().localeCompare(b.toString())},"string-ins":function(a,b){a=a.toString().toLocaleLowerCase();b=b.toString().toLocaleLowerCase();return a.localeCompare(b)}};c.fn.stupidtable_settings=function(a){return this.each(function(){var b=c(this),f=c.extend({},c.fn.stupidtable.default_settings,a);b.stupidtable.settings=f})};c.fn.stupidsort=function(a){var b=c(this),f=b.data("sort")||
null;if(null!==f){var d=b.closest("table"),e={$th:b,$table:d,datatype:f};d.stupidtable.settings||(d.stupidtable.settings=c.extend({},c.fn.stupidtable.default_settings));e.compare_fn=d.data("sortFns")[f];e.th_index=h(e);e.sort_dir=k(a,e);b.data("sort-dir",e.sort_dir);d.trigger("beforetablesort",{column:e.th_index,direction:e.sort_dir,$th:b});d.css("display");setTimeout(function(){d.stupidtable.settings.will_manually_build_table||d.stupidtable_build();var a=l(e),a=m(a,e);if(d.stupidtable.settings.should_redraw(e)){d.children("tbody").append(a);
var a=e.$table,c=e.$th,f=c.data("sort-dir");a.find("th").data("sort-dir",null).removeClass("sorting-desc sorting-asc");c.data("sort-dir",f).addClass("sorting-"+f);d.trigger("aftertablesort",{column:e.th_index,direction:e.sort_dir,$th:b});d.css("display")}},10);return b}};c.fn.updateSortVal=function(a){var b=c(this);b.is("[data-sort-value]")&&b.attr("data-sort-value",a);b.data("sort-value",a);return b};c.fn.stupidtable_build=function(){return this.each(function(){var a=c(this),b=[];a.children("tbody").children("tr").each(function(a,
d){var e={$tr:c(d),columns:[],index:a};c(d).children("td").each(function(a,b){var d=c(b).data("sort-value");"undefined"===typeof d&&(d=c(b).text(),c(b).data("sort-value",d));e.columns.push(d)});b.push(e)});a.data("stupidsort_internaltable",b)})};var l=function(a){var b=a.$table.data("stupidsort_internaltable"),f=a.th_index,d=a.$th.data("sort-multicolumn"),d=d?d.split(","):[],e=c.map(d,function(b,d){var c=a.$table.find("th"),e=parseInt(b,10),f;e||0===e?f=c.eq(e):(f=c.siblings("#"+b),e=c.index(f));
return{index:e,$e:f}});b.sort(function(b,c){for(var d=e.slice(0),g=a.compare_fn(b.columns[f],c.columns[f]);0===g&&d.length;){var g=d[0],h=g.$e.data("sort"),g=(0,a.$table.data("sortFns")[h])(b.columns[g.index],c.columns[g.index]);d.shift()}return 0===g?b.index-c.index:g});a.sort_dir!=c.fn.stupidtable.dir.ASC&&b.reverse();return b},m=function(a,b){var f=c.map(a,function(a,c){return[[a.columns[b.th_index],a.$tr,c]]});b.column=f;return c.map(a,function(a){return a.$tr})},k=function(a,b){var f,d=b.$th,
e=c.fn.stupidtable.dir;a?f=a:(f=a||d.data("sort-default")||e.ASC,d.data("sort-dir")&&(f=d.data("sort-dir")===e.ASC?e.DESC:e.ASC));return f},h=function(a){var b=0,f=a.$th.index();a.$th.parents("tr").find("th").slice(0,f).each(function(){var a=c(this).attr("colspan")||1;b+=parseInt(a,10)});return b}})(jQuery);

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB