blob: adf3171c07a690cbb79a84fab867f53058d29748 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001#!/usr/bin/python3
2
3import smtplib
4import os
5import mysql.connector
6from email.mime.multipart import MIMEMultipart
7from email.mime.text import MIMEText
8from email.utils import COMMASPACE, formatdate
9import cgi
10import jinja2
11from jinja2 import Template
12import json
13import redis
14import time
15import html2text
16import socket
17
18while True:
19 try:
20 r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0)
21 r.ping()
22 except Exception as ex:
23 print('%s - trying again...' % (ex))
24 time.sleep(3)
25 else:
26 break
27
28time_now = int(time.time())
29mailcow_hostname = '__MAILCOW_HOSTNAME__'
30
31max_score = float(r.get('Q_MAX_SCORE') or "9999.0")
32if max_score == "":
33 max_score = 9999.0
34
35def query_mysql(query, headers = True, update = False):
36 while True:
37 try:
38 cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user='__DBUSER__', passwd='__DBPASS__', database='__DBNAME__', charset="utf8")
39 except Exception as ex:
40 print('%s - trying again...' % (ex))
41 time.sleep(3)
42 else:
43 break
44 cur = cnx.cursor()
45 cur.execute(query)
46 if not update:
47 result = []
48 columns = tuple( [d[0] for d in cur.description] )
49 for row in cur:
50 if headers:
51 result.append(dict(list(zip(columns, row))))
52 else:
53 result.append(row)
54 cur.close()
55 cnx.close()
56 return result
57 else:
58 cnx.commit()
59 cur.close()
60 cnx.close()
61
62def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
63 if category == "add_header": category = "add header"
64 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))
65 print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
66 if len(meta_query) == 0:
67 return
68 msg_count = len(meta_query)
69 if r.get('Q_HTML'):
70 try:
71 template = Template(r.get('Q_HTML'))
72 except:
73 print("Error: Cannot parse quarantine template, falling back to default template.")
74 with open('/templates/quarantine.tpl') as file_:
75 template = Template(file_.read())
76 else:
77 with open('/templates/quarantine.tpl') as file_:
78 template = Template(file_.read())
79 html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
80 text = html2text.html2text(html)
81 count = 0
82 while count < 15:
83 count += 1
84 try:
85 server = smtplib.SMTP('postfix', 590, 'quarantine')
86 server.ehlo()
87 msg = MIMEMultipart('alternative')
88 msg_from = r.get('Q_SENDER') or "quarantine@localhost"
89 # Remove non-ascii chars from field
90 msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from])
91 msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification"
92 msg['Date'] = formatdate(localtime = True)
93 text_part = MIMEText(text, 'plain', 'utf-8')
94 html_part = MIMEText(html, 'html', 'utf-8')
95 msg.attach(text_part)
96 msg.attach(html_part)
97 msg['To'] = str(rcpt)
98 bcc = r.get('Q_BCC') or ""
99 redirect = r.get('Q_REDIRECT') or ""
100 text = msg.as_string()
101 if bcc == '':
102 if redirect == '':
103 server.sendmail(msg['From'], str(rcpt), text)
104 else:
105 server.sendmail(msg['From'], str(redirect), text)
106 else:
107 if redirect == '':
108 server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text)
109 else:
110 server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
111 server.quit()
112 for res in meta_query:
113 query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
114 r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
115 break
116 except Exception as ex:
117 server.quit()
118 print('%s' % (ex))
119 time.sleep(3)
120
121records = 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))
122
123for record in records:
124 attrs = ''
125 attrs_json = ''
126 time_trans = {
127 "hourly": 3600,
128 "daily": 86400,
129 "weekly": 604800
130 }
131 try:
132 last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt']))
133 if last_notification > time_now:
134 print('Last notification is > time now, assuming never')
135 last_notification = 0
136 except Exception as ex:
137 print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
138 last_notification = 0
139 attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
140 attrs = attrs_json[0]['attributes']
141 if isinstance(attrs, str):
142 # if attr is str then just load it
143 attrs = json.loads(attrs)
144 else:
145 # if it's bytes then decode and load it
146 attrs = json.loads(attrs.decode('utf-8'))
147 if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'):
148 continue
149 if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) < time_now:
150 print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification']))
151 notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category'])