Mini Shell
Direktori : /opt/sharedrads/ |
|
Current File : //opt/sharedrads/clean_exim.py |
#!/opt/imh-python/bin/python3
"""Python exim cleanup script to reduce the urge to run hacky oneliners"""
import argparse
import re
import subprocess
import sys
from collections import defaultdict
from typing import Union
# compile regex once and recycle them throughout the script for efficiency
EXIM_BP_RE = re.compile(
r'\s*(?P<age>[0-9]+[mhd])\s+[0-9\.]+[M|K]? ' # 5m 15K
r'(?P<id>[a-zA-Z0-9\-]+)\s+' # 1XEhho-0006AU-Tx
r'\<(?P<sender>.*)\>' # <emailuser@domain>
)
def run(
command: Union[list[str], str], shell=False
) -> subprocess.CompletedProcess:
"""
Run a process with arguments.
Optionally, as a shell command
returns CompletedProcess.
"""
args = command
result = subprocess.run(
args,
capture_output=True,
shell=shell,
errors="surrogateescape",
encoding="utf-8",
check=False,
)
return result
def get_queue(exclude_bounces=False, bounces_only=False):
"""Get current exim queue as a list of dicts, each dict
containing a single message, with keys 'age', 'id', and 'sender'"""
queue = []
out = run(["exim", "-bp"])
queue_lines = out.stdout.splitlines()
for line in queue_lines:
match = EXIM_BP_RE.match(line)
if match is None:
continue
groupdict = match.groupdict()
if exclude_bounces and groupdict['sender'] == '':
continue
if bounces_only and groupdict['sender'] != '':
continue
groupdict['age'] = exim_age_to_secs(groupdict['age'])
if groupdict['age'] > 3600:
queue.append(groupdict)
return queue
def exim_age_to_secs(age):
"""convert exim age to seconds"""
conversions = {'m': 60, 'h': 3600, 'd': 86400}
# find the multiplier based on above conversions
multiplier = conversions[age[-1]]
# typecast to int and use multiplier above
return int(age.rstrip('mhd')) * multiplier
def is_boxtrapper(msg_id):
"""Determine if a message ID is a boxtrapper msg in queue"""
try:
out = run(['exim', '-Mvh', msg_id])
out.check_returncode()
head = out.stdout.strip()
except subprocess.CalledProcessError:
return False # likely no longer in queue
return 'Subject: Your email requires verification verify#' in head
def remove_msg(msg_id):
"""Here, msg_id may be a list or just one string"""
if isinstance(msg_id, list):
msg_id = ' '.join(msg_id)
try:
run(['exim', '-Mrm', msg_id]).check_returncode()
except subprocess.CalledProcessError:
pass # may have already been removed from queue or sent
def print_removed(removed):
"""Given a dict of user:count mappings, print removed"""
if len(list(removed.keys())) == 0:
print('None to remove')
else:
print('Removed:')
for user, count in removed.items():
print(' %s: %d' % (user, count))
def clean_boxtrapper():
"""Remove old boxtrapper messages from queue. The theory is that if
still stuck in queue after min_age_secs, they were likely sent from
an illigitimate email address and will never clear from queue normally"""
print('Removing old boxtrapper messages...')
removed = defaultdict(int)
queue = get_queue(exclude_bounces=True)
queue = [x for x in queue if is_boxtrapper(x['id'])]
for msg in queue:
remove_msg(msg['id'])
removed[msg['sender']] += 1
print_removed(removed)
def get_full_quota_user(msg_id):
"""Get the user for a message which is at full quota, or None"""
try:
out = run(['exim', '-Mvb', msg_id])
out.check_returncode()
body = out.stdout.strip()
except subprocess.CalledProcessError:
return None
if not ' Mailbox quota exceeded' in body:
return None
body = body.splitlines()
addr_line = -1
for line_num, line in enumerate(body):
if ' Mailbox quota exceeded' in line:
addr_line = line_num - 1
break
if addr_line >= 0:
return body[addr_line].strip()
return None
def clean_full_inbox_bounces():
"""Remove "mailbox quota exceeded" bounces from queue"""
print('Removing old full inbox bounces...')
removed = defaultdict(int)
queue = get_queue(bounces_only=True)
for msg in queue:
user = get_full_quota_user(msg['id'])
if user is None:
continue
remove_msg(msg['id'])
removed[user] += 1
print_removed(removed)
def clean_autoresponders():
"""Remove old auto-responder emails from queue which are
likely responses to spam if they have been stuck in queue"""
print('Removing old auto-responses...')
removed = defaultdict(int)
queue = get_queue(exclude_bounces=True)
for msg in queue:
try:
out = run(['exim', '-Mvh', msg['id']])
out.check_returncode()
headers = out.stdout.strip()
except subprocess.CalledProcessError:
continue
if 'X-Autorespond:' in headers and 'auto_reply' in headers:
remove_msg(msg['id'])
removed[msg['sender']] += 1
print_removed(removed)
def _parse_args():
"""Parse commandline arguments"""
parser = argparse.ArgumentParser(
description="Exim cleanup tool",
epilog="One and only one option is allowed",
)
parser.add_argument(
'-a', '--all', action='store_true', help='Do all cleanup procedures'
)
parser.add_argument(
'-b',
'--boxtrapper',
action='store_true',
help='Remove old boxtrapper messages',
)
parser.add_argument(
'-r',
'--autorespond',
action='store_true',
help='Remove old auto-responder messages',
)
parser.add_argument(
'-f',
'--full',
action='store_true',
help='Remove old bounces for full inbox',
)
args = parser.parse_args()
chosen = [key for key, value in vars(args).items() if value is True]
if len(chosen) != 1:
parser.print_help()
sys.exit(1)
return args
def main():
"""Main logic"""
args = _parse_args()
if args.boxtrapper or args.all:
clean_boxtrapper()
if args.full or args.all:
clean_full_inbox_bounces()
if args.autorespond or args.all:
clean_autoresponders()
if __name__ == '__main__':
main()
Zerion Mini Shell 1.0