blob: 65f2a0e60faa83f088c5629e98264b09a7af07ba [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001#!/usr/bin/python3
2
3import smtplib
4import os
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02005import sys
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01006import mysql.connector
7from email.mime.multipart import MIMEMultipart
8from email.mime.text import MIMEText
9from email.utils import COMMASPACE, formatdate
10import cgi
11import jinja2
12from jinja2 import Template
13import json
14import redis
15import time
16import html2text
17import socket
18
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020019pid = str(os.getpid())
20pidfile = "/tmp/quarantine_notify.pid"
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010021
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020022if os.path.isfile(pidfile):
23 print("%s already exists, exiting" % (pidfile))
24 sys.exit()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010025
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020026pid = str(os.getpid())
27f = open(pidfile, 'w')
28f.write(pid)
29f.close()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010030
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020031try:
32
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010033 while True:
34 try:
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020035 r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0)
36 r.ping()
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010037 except Exception as ex:
38 print('%s - trying again...' % (ex))
39 time.sleep(3)
40 else:
41 break
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010042
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020043 time_now = int(time.time())
44 mailcow_hostname = os.environ.get('MAILCOW_HOSTNAME')
45
46 max_score = float(r.get('Q_MAX_SCORE') or "9999.0")
47 if max_score == "":
48 max_score = 9999.0
49
50 def query_mysql(query, headers = True, update = False):
51 while True:
52 try:
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010053 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")
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020054 except Exception as ex:
55 print('%s - trying again...' % (ex))
56 time.sleep(3)
57 else:
58 break
59 cur = cnx.cursor()
60 cur.execute(query)
61 if not update:
62 result = []
63 columns = tuple( [d[0] for d in cur.description] )
64 for row in cur:
65 if headers:
66 result.append(dict(list(zip(columns, row))))
67 else:
68 result.append(row)
69 cur.close()
70 cnx.close()
71 return result
72 else:
73 cnx.commit()
74 cur.close()
75 cnx.close()
76
77 def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
78 if category == "add_header": category = "add header"
79 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))
80 print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
81 if len(meta_query) == 0:
82 return
83 msg_count = len(meta_query)
84 if r.get('Q_HTML'):
85 try:
86 template = Template(r.get('Q_HTML'))
87 except:
88 print("Error: Cannot parse quarantine template, falling back to default template.")
89 with open('/templates/quarantine.tpl') as file_:
90 template = Template(file_.read())
91 else:
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010092 with open('/templates/quarantine.tpl') as file_:
93 template = Template(file_.read())
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020094 html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
95 text = html2text.html2text(html)
96 count = 0
97 while count < 15:
98 count += 1
99 try:
100 server = smtplib.SMTP('postfix', 590, 'quarantine')
101 server.ehlo()
102 msg = MIMEMultipart('alternative')
103 msg_from = r.get('Q_SENDER') or "quarantine@localhost"
104 # Remove non-ascii chars from field
105 msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from])
106 msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification"
107 msg['Date'] = formatdate(localtime = True)
108 text_part = MIMEText(text, 'plain', 'utf-8')
109 html_part = MIMEText(html, 'html', 'utf-8')
110 msg.attach(text_part)
111 msg.attach(html_part)
112 msg['To'] = str(rcpt)
113 bcc = r.get('Q_BCC') or ""
114 redirect = r.get('Q_REDIRECT') or ""
115 text = msg.as_string()
116 if bcc == '':
117 if redirect == '':
118 server.sendmail(msg['From'], str(rcpt), text)
119 else:
120 server.sendmail(msg['From'], str(redirect), text)
121 else:
122 if redirect == '':
123 server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text)
124 else:
125 server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
126 server.quit()
127 for res in meta_query:
128 query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
129 r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
130 break
131 except Exception as ex:
132 server.quit()
133 print('%s' % (ex))
134 time.sleep(3)
135
136 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))
137
138 for record in records:
139 attrs = ''
140 attrs_json = ''
141 time_trans = {
142 "hourly": 3600,
143 "daily": 86400,
144 "weekly": 604800
145 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100146 try:
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200147 last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt']))
148 if last_notification > time_now:
149 print('Last notification is > time now, assuming never')
150 last_notification = 0
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100151 except Exception as ex:
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200152 print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100153 last_notification = 0
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200154 attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
155 attrs = attrs_json[0]['attributes']
156 if isinstance(attrs, str):
157 # if attr is str then just load it
158 attrs = json.loads(attrs)
159 else:
160 # if it's bytes then decode and load it
161 attrs = json.loads(attrs.decode('utf-8'))
162 if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'):
163 continue
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100164 if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) <= time_now:
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200165 print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification']))
166 notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category'])
167
168finally:
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100169 os.unlink(pidfile)