Files
iRedAdmin-Pro-SQL/libs/amavisd/log.py
2023-04-10 07:18:32 +02:00

573 lines
22 KiB
Python

# Author: Zhang Huangbin <zhb@iredmail.org>
import time
import web
import settings
from libs import iredutils
from libs.logger import logger, log_traceback
from libs.amavisd import MAIL_ID_CHARACTERS
session = web.config.get('_session')
# Import backend related modules.
if settings.backend == 'ldap':
from libs.ldaplib import admin as ldap_lib_admin
elif settings.backend in ['mysql', 'pgsql']:
from libs.sqllib import admin as sql_lib_admin
def delete_all_records(log_type=None, account=None):
# Delete all records, or delete records older than one week.
# :param log_type: sent, received
# :param account: single email address, domain name, '@.'
account_is_email = False
account_is_domain = False
maddr_ids = []
managed_domains_reversed = []
if account:
if iredutils.is_email(account):
account_is_email = True
elif iredutils.is_domain(account):
account_is_domain = True
else:
if account == '@.':
pass
else:
return False, 'INVALID_ACCOUNT'
# get `maddr.id` of this account
if account_is_email:
# user
qr = web.conn_amavisd.select('maddr',
vars={'account': account},
what='id',
where='email=$account',
limit=1)
if qr:
maddr_ids.append(qr[0].id)
elif account_is_domain:
# domain
reversed_domain = iredutils.reverse_amavisd_domain_names([account])[0]
qr = web.conn_amavisd.select('maddr',
vars={'account': reversed_domain},
what='id',
where='domain=$account')
if qr:
for r in qr:
maddr_ids.append(r.id)
# no `maddr.id`, no mail log.
if not maddr_ids:
return True,
else:
if session.get('is_global_admin'):
web.conn_amavisd.delete('msgs', where='1=1')
web.conn_amavisd.delete('msgrcpt', where='1=1')
return True,
# Get all managed domains by normal admin.
managed_domains = []
if settings.backend == 'ldap':
_qr = ldap_lib_admin.get_managed_domains(admin=session.get('username'))
if _qr[0]:
managed_domains = _qr[1]
elif settings.backend in ['mysql', 'pgsql']:
qr = sql_lib_admin.get_managed_domains(admin=session.get('username'),
domain_name_only=True)
if qr[0]:
managed_domains = qr[1]
else:
return False, 'UNKNOWN_BACKEND'
managed_domains_reversed = iredutils.reverse_amavisd_domain_names(managed_domains)
if not managed_domains_reversed:
return True,
try:
# Delete records in tables: msgs, msgrcpt.
if log_type == 'sent':
if account:
# Delete all records sent by single user
web.conn_amavisd.delete('msgs',
vars={'maddr_ids': maddr_ids},
where='sid IN $maddr_ids')
else:
# Delete all records sent by domain users
web.conn_amavisd.delete('msgs',
vars={'managed_domains_reversed': managed_domains_reversed},
where='sid IN (SELECT id FROM maddr WHERE domain IN $managed_domains_reversed)')
elif log_type == 'received':
if account:
web.conn_amavisd.delete('msgs',
vars={'maddr_ids': maddr_ids},
where='mail_id IN (SELECT mail_id FROM msgrcpt WHERE rid IN $maddr_ids)')
web.conn_amavisd.delete('msgrcpt',
vars={'maddr_ids': maddr_ids},
where='rid IN $maddr_ids')
else:
all_rcpt_ids = [] # maddr.id
qr = web.conn_amavisd.select('maddr',
vars={'domains': managed_domains_reversed},
what='id',
where='domain IN $domains')
for i in qr:
all_rcpt_ids.append(i['id'])
del qr
web.conn_amavisd.delete('msgs',
vars={'ids': all_rcpt_ids},
where='mail_id IN (SELECT mail_id FROM msgrcpt WHERE rid IN $ids)')
web.conn_amavisd.delete('msgrcpt',
vars={'ids': all_rcpt_ids},
where='rid IN $ids')
del all_rcpt_ids
return True,
except Exception as e:
return False, repr(e)
def delete_records_by_mail_id(log_type='sent', mail_ids=None):
# log_type -- received, sent, quarantined, quarantine
if not isinstance(mail_ids, list):
return False, 'INCORRECT_MAILID'
# Filter unexpected mail_id strings.
mail_ids = [v for v in mail_ids if len(set(v) - set(MAIL_ID_CHARACTERS)) == 0]
if not mail_ids:
return True,
# Converted into SQL style list.
mail_ids = web.sqlquote(mail_ids)
if log_type in ['received', 'sent', 'quarantined', 'quarantine']:
try:
# Delete records in tables: msgs, msgrcpt.
web.conn_amavisd.delete('msgs', where='mail_id IN %s' % mail_ids)
web.conn_amavisd.delete('msgrcpt', where='mail_id IN %s' % mail_ids)
except Exception as e:
return False, repr(e)
if log_type in ['quarantined', 'quarantine']:
try:
web.conn_amavisd.delete('quarantine', where="mail_id IN %s" % mail_ids)
except Exception as e:
return False, repr(e)
return True,
def count_incoming_mails(reversedDomainNames=None,
timeLength=None,
sqlAppendWhere=None):
# timeLength is seconds.
total = 0
if not reversedDomainNames:
if not session.get('account_is_mail_user'):
return total
if sqlAppendWhere:
sql_append_where = sqlAppendWhere
else:
sql_append_where = ' AND recip.domain IN %s' % web.sqlquote(reversedDomainNames)
if isinstance(timeLength, int):
_now = int(time.time())
_length_seconds = _now - timeLength
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
try:
qr = web.conn_amavisd.query('''
-- Get number of incoming mails.
SELECT COUNT(msgs.mail_id) AS total
FROM msgs
LEFT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
LEFT JOIN maddr AS sender ON (msgs.sid = sender.id)
LEFT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
WHERE msgs.quar_type <> 'Q' %s
''' % sql_append_where)
total = qr[0].total or 0
except Exception as e:
logger.error(e)
return total
def count_outgoing_mails(reversedDomainNames=None,
timeLength=None,
sqlAppendWhere=None):
# timeLength is seconds.
total = 0
sql_append_where = ''
if not reversedDomainNames:
return total
if sqlAppendWhere:
sql_append_where = sqlAppendWhere
else:
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversedDomainNames)
if isinstance(timeLength, int):
_now = int(time.time())
_length_seconds = _now - timeLength
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
try:
qr_count = web.conn_amavisd.query("""
-- Get number of outgoing mails.
SELECT COUNT(msgs.mail_id) AS total
FROM msgs
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
WHERE msgs.quar_type <> 'Q' %s""" % sql_append_where)
total = qr_count[0].total or 0
except Exception:
pass
return total
def count_virus_mails(reversedDomainNames=None, timeLength=None):
# timeLength is seconds.
total = 0
sql_append_where = ''
if not reversedDomainNames:
return total
if session.get('is_global_admin') is not True:
sql_append_where += ' AND (sender.domain IN {} OR recip.domain IN {})'.format(
web.sqlquote(reversedDomainNames),
web.sqlquote(reversedDomainNames),
)
if isinstance(timeLength, int):
_now = int(time.time())
_length_seconds = _now - timeLength
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
try:
qr = web.conn_amavisd.query("""
SELECT COUNT(msgs.mail_id) AS total
FROM msgs
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
WHERE msgs.content = 'V'
AND msgs.quar_type='Q'
%s
""" % sql_append_where)
total = qr[0].total or 0
except Exception:
pass
return total
def count_quarantined(reversedDomainNames=None, timeLength=None):
# timeLength is seconds.
total = 0
sql_append_where = ''
if not session.get('is_global_admin'):
sql_append_where += ' AND (sender.domain IN {} OR recip.domain IN {})'.format(
web.sqlquote(reversedDomainNames),
web.sqlquote(reversedDomainNames),
)
if isinstance(timeLength, int):
_now = int(time.time())
_length_seconds = _now - timeLength
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
try:
if session.get('is_global_admin'):
qr = web.conn_amavisd.query("""
SELECT COUNT(msgs.mail_id) AS total
FROM msgs
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
WHERE msgs.quar_type = 'Q' %s
""" % sql_append_where)
else:
qr = web.conn_amavisd.query("""
SELECT COUNT(msgs.mail_id) AS total
FROM msgs
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
WHERE msgs.quar_type = 'Q' %s
""" % sql_append_where)
total = qr[0].total or 0
except:
log_traceback()
return total
def get_in_out_mails(log_type='sent',
cur_page=1,
account_type='',
account='',
page_size_limit=None):
"""
@account_type: 'domain', 'user', None
@log_type: 'sent', 'received', 'all'
@return (True, {'count': <int>, 'records': <list>}
"""
log_type = str(log_type)
cur_page = int(cur_page)
account_type = str(account_type) or None
account = str(account) or None
result = {'count': 0, 'records': []}
count = 0 # Number of total mails.
records = {} # Detail records.
sql_append_where = ''
reversed_account = ''
if not page_size_limit:
page_size_limit = settings.PAGE_SIZE_LIMIT
if account_type == 'domain':
reversed_account = iredutils.reverse_amavisd_domain_names([account])
# Get all managed domain names and reversed names.
all_domains = []
allReversedDomainNames = []
quoted_all_reversed_domain_names = []
sql_restricted_sender_domains = ''
sql_restricted_recip_domains = ''
if not session.get('account_is_mail_user'):
if settings.backend == 'ldap':
_qr = ldap_lib_admin.get_managed_domains(admin=session.get('username'))
if _qr[0]:
all_domains = _qr[1]
elif settings.backend in ['mysql', 'pgsql']:
qr_all_domains = sql_lib_admin.get_managed_domains(admin=session.get('username'),
domain_name_only=True)
if qr_all_domains[0]:
all_domains += qr_all_domains[1]
else:
result['count'] = count
result['records'] = list(records)
return True, result
allReversedDomainNames = iredutils.reverse_amavisd_domain_names(all_domains)
quoted_all_reversed_domain_names = web.sqlquote(allReversedDomainNames)
sql_restricted_sender_domains = ' AND sender.domain IN %s' % quoted_all_reversed_domain_names
sql_restricted_recip_domains = ' AND recip.domain IN %s' % quoted_all_reversed_domain_names
# restrict permission for per-account search
# @log_type == 'sent'
# - if domain is under control, no restriction
# - if domain is not under control, restrict recipient domain to managed domains
# @log_type == 'received'
# - if domain is under control, no restriction
# - if domain is not under control, restrict sender domain to managed domains
verify_domain = account
if account_type == 'user':
verify_domain = account.split('@', 1)[-1]
if log_type == 'received':
if account_type == 'domain':
if session.get('is_global_admin') or verify_domain in all_domains:
sql_append_where += ' AND recip.domain IN %s' % web.sqlquote(reversed_account)
else:
sql_append_where += ' {} AND recip.domain IN {}'.format(sql_restricted_sender_domains, web.sqlquote(reversed_account))
elif account_type == 'user':
if session.get('is_global_admin') or verify_domain in all_domains:
sql_append_where += ' AND recip.email=%s' % web.sqlquote(account)
else:
sql_append_where += ' {} AND recip.email={}'.format(sql_restricted_sender_domains, web.sqlquote(account))
else:
if settings.AMAVISD_SHOW_NON_LOCAL_DOMAINS:
if session.get('is_global_admin'):
pass
else:
if not quoted_all_reversed_domain_names:
return True, result
else:
sql_append_where += ' AND recip.domain IN %s' % quoted_all_reversed_domain_names
else:
if not quoted_all_reversed_domain_names:
return True, result
else:
sql_append_where += ' AND recip.domain IN %s' % quoted_all_reversed_domain_names
elif log_type == 'sent':
if account_type == 'domain':
if session.get('is_global_admin') or verify_domain in all_domains:
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversed_account)
else:
sql_append_where += ' {} AND sender.domain IN {}'.format(sql_restricted_recip_domains, web.sqlquote(reversed_account))
elif account_type == 'user':
if session.get('is_global_admin') or verify_domain in all_domains:
sql_append_where += ' AND sender.email = %s' % (web.sqlquote(account))
else:
sql_append_where += ' {} AND sender.email = {}'.format(sql_restricted_recip_domains, web.sqlquote(account))
else:
if settings.AMAVISD_SHOW_NON_LOCAL_DOMAINS:
if session.get('is_global_admin'):
pass
else:
if not quoted_all_reversed_domain_names:
return True, result
else:
sql_append_where += ' AND sender.domain IN %s' % quoted_all_reversed_domain_names
else:
if not quoted_all_reversed_domain_names:
return True, result
else:
sql_append_where += ' AND sender.domain IN %s' % quoted_all_reversed_domain_names
########################
# Get detail records.
#
try:
if log_type == 'received':
count = count_incoming_mails(allReversedDomainNames,
sqlAppendWhere=sql_append_where)
qr = web.conn_amavisd.query(
'''
-- Get records of received mails.
SELECT
msgs.mail_id, msgs.subject, msgs.time_num,
msgs.size, msgs.spam_level, msgs.client_addr, msgs.policy,
sender.email_raw AS sender_email,
recip.email_raw AS recipient
FROM msgs
LEFT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
LEFT JOIN maddr AS sender ON (msgs.sid = sender.id)
LEFT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
WHERE msgs.quar_type <> 'Q' %s
ORDER BY msgs.time_num DESC
LIMIT %d
OFFSET %d
''' % (sql_append_where,
page_size_limit,
(cur_page - 1) * page_size_limit)
)
records = iredutils.bytes2str(qr)
elif log_type == 'sent':
count = count_outgoing_mails(allReversedDomainNames,
sqlAppendWhere=sql_append_where)
qr = web.conn_amavisd.query(
'''
-- Get records of sent mails.
SELECT
msgs.mail_id, msgs.subject, msgs.time_num,
msgs.size, msgs.client_addr, msgs.policy,
sender.email_raw AS sender_email,
recip.email_raw AS recipient
FROM msgs
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
RIGHT JOIN maddr AS recip ON (msgrcpt.rid = recip.id)
WHERE msgs.quar_type <> 'Q' %s
ORDER BY msgs.time_num DESC
LIMIT %d
OFFSET %d
''' % (sql_append_where,
page_size_limit,
(cur_page - 1) * page_size_limit)
)
records = iredutils.bytes2str(qr)
else:
records = {}
except:
pass
return True, {'count': count, 'records': list(records)}
def get_top_users(reversedDomainNames=None,
log_type='sent',
timeLength=None,
number=10):
records = {}
sql_append_where = ''
if settings.AMAVISD_SHOW_NON_LOCAL_DOMAINS:
if session.get('is_global_admin'):
pass
else:
if not reversedDomainNames:
return []
else:
if log_type == 'sent':
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversedDomainNames)
elif log_type == 'received':
sql_append_where += ' AND rcpt.domain IN %s' % web.sqlquote(reversedDomainNames)
else:
if log_type == 'sent':
sql_append_where += ' AND sender.domain IN %s' % web.sqlquote(reversedDomainNames)
elif log_type == 'received':
sql_append_where += ' AND rcpt.domain IN %s' % web.sqlquote(reversedDomainNames)
if isinstance(timeLength, int):
_now = int(time.time())
_length_seconds = _now - timeLength
sql_append_where += ' AND msgs.time_num > %d' % _length_seconds
# `msgs.policy` (Amavisd policy bank) is used to identify account type.
# for example, 'MLMMJ' means mlmmj mailing list.
if log_type == 'sent':
try:
result = web.conn_amavisd.query(
"""
-- Get top 10 senders.
SELECT COUNT(msgs.mail_id) AS total,
sender.email_raw AS mail,
msgs.policy AS policy
FROM msgs
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
WHERE 1=1 %s
GROUP BY mail, policy
ORDER BY total DESC
LIMIT %d
""" % (sql_append_where, number))
records = list(result)
except:
log_traceback()
elif log_type == 'received':
try:
result = web.conn_amavisd.query(
"""
-- Get top 10 recipients
SELECT COUNT(msgs.mail_id) AS total,
rcpt.email_raw AS mail
FROM msgs
RIGHT JOIN msgrcpt ON (msgs.mail_id = msgrcpt.mail_id)
RIGHT JOIN maddr AS sender ON (msgs.sid = sender.id)
RIGHT JOIN maddr AS rcpt ON (msgrcpt.rid = rcpt.id)
WHERE 1=1 %s
GROUP BY mail
ORDER BY total DESC
LIMIT %d
""" % (sql_append_where, number))
records = list(result)
except:
log_traceback()
records = iredutils.bytes2str(records)
return list(records)