public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Trace
data/Dockerfiles/dovecot/quarantine_notify.py
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
#!/usr/bin/python3
3
import smtplib
4
import os
5
import sys
6
import MySQLdb
7
from email.mime.multipart import MIMEMultipart
8
from email.mime.text import MIMEText
9
from email.utils import COMMASPACE, formatdate
10
import jinja2
11
from jinja2 import TemplateError
12
from jinja2.sandbox import SandboxedEnvironment
13
import json
14
import redis
15
import time
16
import html2text
17
import socket
19
pid = str(os.getpid())
20
pidfile = "/tmp/quarantine_notify.pid"
22
if os.path.isfile(pidfile):
23
print("%s already exists, exiting" % (pidfile))
24
sys.exit()
26
pid = str(os.getpid())
27
f = open(pidfile, 'w')
28
f.write(pid)
29
f.close()
31
try:
33
while True:
34
try:
35
r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
36
r.ping()
37
except Exception as ex:
38
print('%s - trying again...' % (ex))
39
time.sleep(3)
40
else:
41
break
43
time_now = int(time.time())
44
mailcow_hostname = os.environ.get('MAILCOW_HOSTNAME')
46
max_score = float(r.get('Q_MAX_SCORE') or "9999.0")
47
if max_score == "":
48
max_score = 9999.0
50
def query_mysql(query, params = None, headers = True, update = False):
51
while True:
52
try:
53
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
54
except Exception as ex:
55
print('%s - trying again...' % (ex))
56
time.sleep(3)
57
else:
58
break
59
cur = cnx.cursor()
60
if params:
61
cur.execute(query, params)
62
else:
63
cur.execute(query)
64
if not update:
65
result = []
66
columns = tuple( [d[0] for d in cur.description] )
67
for row in cur:
68
if headers:
69
result.append(dict(list(zip(columns, row))))
70
else:
71
result.append(row)
72
cur.close()
73
cnx.close()
74
return result
75
else:
76
cnx.commit()
77
cur.close()
78
cnx.close()
80
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
81
if category == "add_header": category = "add header"
82
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category))
83
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
84
if len(meta_query) == 0:
85
return
86
msg_count = len(meta_query)
87
env = SandboxedEnvironment()
88
if r.get('Q_HTML'):
89
try:
90
template = env.from_string(r.get('Q_HTML'))
91
except Exception:
92
print("Error: Cannot parse quarantine template, falling back to default template.")
93
with open('/templates/quarantine.tpl') as file_:
94
template = env.from_string(file_.read())
95
else:
96
with open('/templates/quarantine.tpl') as file_:
97
template = env.from_string(file_.read())
98
try:
99
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
100
except (jinja2.exceptions.SecurityError, TemplateError) as ex:
101
print(f"SecurityError or TemplateError in template rendering: {ex}")
102
return
103
text = html2text.html2text(html)
104
count = 0
105
while count < 15:
106
count += 1
107
try:
108
server = smtplib.SMTP('postfix', 590, 'quarantine')
109
server.ehlo()
110
msg = MIMEMultipart('alternative')
111
msg_from = r.get('Q_SENDER') or "quarantine@localhost"
112
# Remove non-ascii chars from field
113
msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from])
114
msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification"
115
msg['Date'] = formatdate(localtime = True)
116
text_part = MIMEText(text, 'plain', 'utf-8')
117
html_part = MIMEText(html, 'html', 'utf-8')
118
msg.attach(text_part)
119
msg.attach(html_part)
120
msg['To'] = str(rcpt)
121
bcc = r.get('Q_BCC') or ""
122
redirect = r.get('Q_REDIRECT') or ""
123
text = msg.as_string()
124
if bcc == '':
125
if redirect == '':
126
server.sendmail(msg['From'], str(rcpt), text)
127
else:
128
server.sendmail(msg['From'], str(redirect), text)
129
else:
130
if redirect == '':
131
server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text)
132
else:
133
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
134
server.quit()
135
for res in meta_query:
136
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True)
137
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
138
break
139
except Exception as ex:
140
server.quit()
141
print('%s' % (ex))
142
time.sleep(3)
144
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 < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,))
146
for record in records:
147
attrs = ''
148
attrs_json = ''
149
time_trans = {
150
"hourly": 3600,
151
"daily": 86400,
152
"weekly": 604800
153
}
154
try:
155
last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt']))
156
if last_notification > time_now:
157
print('Last notification is > time now, assuming never')
158
last_notification = 0
159
except Exception as ex:
160
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
161
last_notification = 0
162
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],))
163
attrs = attrs_json[0]['attributes']
164
if isinstance(attrs, str):
165
# if attr is str then just load it
166
attrs = json.loads(attrs)
167
else:
168
# if it's bytes then decode and load it
169
attrs = json.loads(attrs.decode('utf-8'))
170
if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'):
171
continue
172
if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) <= time_now:
173
print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification']))
174
notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category'])
176
finally:
177
os.unlink(pidfile)