#!/usr/bin/python3 import smtplib import os import sys import mysql.connector from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import COMMASPACE, formatdate import cgi import jinja2 from jinja2 import Template import json import redis import time import html2text import socket pid = str(os.getpid()) pidfile = "/tmp/quarantine_notify.pid" if os.path.isfile(pidfile): print("%s already exists, exiting" % (pidfile)) sys.exit() pid = str(os.getpid()) f = open(pidfile, 'w') f.write(pid) f.close() try: while True: try: r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0) r.ping() except Exception as ex: print('%s - trying again...' % (ex)) time.sleep(3) else: break time_now = int(time.time()) mailcow_hostname = os.environ.get('MAILCOW_HOSTNAME') max_score = float(r.get('Q_MAX_SCORE') or "9999.0") if max_score == "": max_score = 9999.0 def query_mysql(query, headers = True, update = False): while True: try: cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci") except Exception as ex: print('%s - trying again...' % (ex)) time.sleep(3) else: break cur = cnx.cursor() cur.execute(query) if not update: result = [] columns = tuple( [d[0] for d in cur.description] ) for row in cur: if headers: result.append(dict(list(zip(columns, row)))) else: result.append(row) cur.close() cnx.close() return result else: cnx.commit() cur.close() cnx.close() def notify_rcpt(rcpt, msg_count, quarantine_acl, category): if category == "add_header": category = "add header" meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) if len(meta_query) == 0: return msg_count = len(meta_query) if r.get('Q_HTML'): try: template = Template(r.get('Q_HTML')) except: print("Error: Cannot parse quarantine template, falling back to default template.") with open('/templates/quarantine.tpl') as file_: template = Template(file_.read()) else: with open('/templates/quarantine.tpl') as file_: template = Template(file_.read()) html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) text = html2text.html2text(html) count = 0 while count < 15: count += 1 try: server = smtplib.SMTP('postfix', 590, 'quarantine') server.ehlo() msg = MIMEMultipart('alternative') msg_from = r.get('Q_SENDER') or "quarantine@localhost" # Remove non-ascii chars from field msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from]) msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification" msg['Date'] = formatdate(localtime = True) text_part = MIMEText(text, 'plain', 'utf-8') html_part = MIMEText(html, 'html', 'utf-8') msg.attach(text_part) msg.attach(html_part) msg['To'] = str(rcpt) bcc = r.get('Q_BCC') or "" redirect = r.get('Q_REDIRECT') or "" text = msg.as_string() if bcc == '': if redirect == '': server.sendmail(msg['From'], str(rcpt), text) else: server.sendmail(msg['From'], str(redirect), text) else: if redirect == '': server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text) else: server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text) server.quit() for res in meta_query: query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True) r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now) break except Exception as ex: server.quit() print('%s' % (ex)) time.sleep(3) records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score)) for record in records: attrs = '' attrs_json = '' time_trans = { "hourly": 3600, "daily": 86400, "weekly": 604800 } try: last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt'])) if last_notification > time_now: print('Last notification is > time now, assuming never') last_notification = 0 except Exception as ex: print('Could not determine last notification for %s, assuming never' % (record['rcpt'])) last_notification = 0 attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt'])) attrs = attrs_json[0]['attributes'] if isinstance(attrs, str): # if attr is str then just load it attrs = json.loads(attrs) else: # if it's bytes then decode and load it attrs = json.loads(attrs.decode('utf-8')) if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'): continue if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) <= time_now: print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification'])) notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) finally: os.unlink(pidfile)