8 Commits
0.0.7 ... 0.0.9

Author SHA1 Message Date
182ca2bad7 Change version to 0.0.9 2020-02-01 21:08:12 +01:00
1508d39ed8 Fix encoding issues 2020-02-01 19:53:07 +01:00
42536befdb Fix typo in notifications.py 2020-01-29 22:26:30 +01:00
d09a453f3d Extend notification.template
Signed-off-by: Thomas Oettli <spacefreak@noop.ch>
2020-01-29 21:42:27 +01:00
983362a69a Add decoding of mail headers 2020-01-29 21:35:37 +01:00
f4399312b4 Add url encoded email template variables 2020-01-29 19:58:08 +01:00
b40e835215 Fix error during decoding of mail body 2020-01-27 22:47:57 +01:00
057e66f945 Fix CLI once more 2020-01-21 16:23:59 +01:00
6 changed files with 85 additions and 54 deletions

View File

@@ -77,11 +77,17 @@ The following configuration options are optional in each quarantine section:
The following template variables are available: The following template variables are available:
* **{EMAIL_ENVELOPE_FROM}** * **{EMAIL_ENVELOPE_FROM}**
E-mail from-address received by the milter. E-mail sender address received by the milter.
* **{EMAIL_ENVELOPE_FROM_URL}**
Like EMAIL_ENVELOPE_FROM, but URL encoded
* **{EMAIL_FROM}** * **{EMAIL_FROM}**
Value of the from header of the original e-mail. Value of the FROM header of the original e-mail.
* **{EMAIL_TO}** * **{EMAIL_ENVELOPE_TO}**
E-mail recipient address of this notification. E-mail recipient address of this notification.
* **{EMAIL_ENVELOPE_TO_URL}**
Like EMAIL_ENVELOPE_TO, but URL encoded
* **{EMAIL_TO}**
Value of the TO header of the original e-mail.
* **{EMAIL_SUBJECT}** * **{EMAIL_SUBJECT}**
Configured e-mail subject. Configured e-mail subject.
* **{EMAIL_QUARANTINE_ID}** * **{EMAIL_QUARANTINE_ID}**

View File

@@ -10,6 +10,10 @@
<td><b>From:</b></td> <td><b>From:</b></td>
<td>{EMAIL_FROM}</td> <td>{EMAIL_FROM}</td>
</tr> </tr>
<tr>
<td><b>Envelope-To:</b></td>
<td>{EMAIL_ENVELOPE_TO}</td>
</tr>
<tr> <tr>
<td><b>To:</b></td> <td><b>To:</b></td>
<td>{EMAIL_TO}</td> <td>{EMAIL_TO}</td>

View File

@@ -95,7 +95,8 @@ class QuarantineMilter(Milter.Base):
def set_configfiles(config_files): def set_configfiles(config_files):
QuarantineMilter._config_files = config_files QuarantineMilter._config_files = config_files
def connect(self, IPname, family, hostaddr): def connect(self, hostname, family, hostaddr):
self.hostaddr = hostaddr
self.logger.debug( self.logger.debug(
"accepted milter connection from {} port {}".format( "accepted milter connection from {} port {}".format(
*hostaddr)) *hostaddr))
@@ -134,15 +135,33 @@ class QuarantineMilter(Milter.Base):
"{}: received queue-id from MTA".format(self.queueid)) "{}: received queue-id from MTA".format(self.queueid))
self.recipients = list(self.recipients) self.recipients = list(self.recipients)
self.headers = [] self.headers = []
self.logger.debug(
"{}: initializing memory buffer to save email data".format(
self.queueid))
# initialize memory buffer to save email data
self.fp = BytesIO()
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def header(self, name, value): def header(self, name, value):
self.headers.append((name, value)) try:
# write email header to memory buffer
self.fp.write("{}: {}\r\n".format(name, value).encode(
encoding="ascii", errors="surrogateescape"))
# keep copy of header without surrogates for later use
self.headers.append((
name.encode(errors="surrogateescape").decode(errors="replace"),
value.encode(errors="surrogateescape").decode(errors="replace")))
except Exception as e:
self.logger.exception(
"an exception occured in header function: {}".format(e))
return Milter.TEMPFAIL
return Milter.CONTINUE return Milter.CONTINUE
def eoh(self): def eoh(self):
try: try:
self.fp.write("\r\n".encode(encoding="ascii"))
self.whitelist_cache = whitelists.WhitelistCache() self.whitelist_cache = whitelists.WhitelistCache()
# initialize dicts to set quaranines per recipient and keep matches # initialize dicts to set quaranines per recipient and keep matches
@@ -238,26 +257,14 @@ class QuarantineMilter(Milter.Base):
self.queueid)) self.queueid))
return Milter.ACCEPT return Milter.ACCEPT
# check if the email body is needed # check if the mail body is needed
keep_body = False
for recipient, quarantine in self.recipients_quarantines.items(): for recipient, quarantine in self.recipients_quarantines.items():
if quarantine["quarantine_obj"] or quarantine["notification_obj"]: if quarantine["quarantine_obj"] or quarantine["notification_obj"]:
keep_body = True # mail body is needed, continue processing
break return Milter.CONTINUE
if keep_body:
self.logger.debug(
"{}: initializing memory buffer to save email data".format(
self.queueid))
# initialize memory buffer to save email data
self.fp = BytesIO()
# write email headers to memory buffer
for name, value in self.headers:
self.fp.write("{}: {}\n".format(name, value).encode())
self.fp.write("\n".encode())
else:
# quarantine and notification are disabled on all matching # quarantine and notification are disabled on all matching
# quarantines, return configured action # quarantines, just return configured action
quarantine = self._get_preferred_quarantine() quarantine = self._get_preferred_quarantine()
self.logger.info( self.logger.info(
"{}: {} matching quarantine is '{}', performing milter action {}".format( "{}: {} matching quarantine is '{}', performing milter action {}".format(
@@ -269,8 +276,6 @@ class QuarantineMilter(Milter.Base):
self.setreply("554", "5.7.0", quarantine["reject_reason"]) self.setreply("554", "5.7.0", quarantine["reject_reason"])
return quarantine["milter_action"] return quarantine["milter_action"]
return Milter.CONTINUE
except Exception as e: except Exception as e:
self.logger.exception( self.logger.exception(
"an exception occured in eoh function: {}".format(e)) "an exception occured in eoh function: {}".format(e))
@@ -372,6 +377,11 @@ class QuarantineMilter(Milter.Base):
"an exception occured in eom function: {}".format(e)) "an exception occured in eom function: {}".format(e))
return Milter.TEMPFAIL return Milter.TEMPFAIL
def close(self):
self.logger.debug(
"disconnect from {} port {}".format(
*self.hostaddr))
def generate_milter_config(configtest=False, config_files=[]): def generate_milter_config(configtest=False, config_files=[]):
"Generate the configuration for QuarantineMilter class." "Generate the configuration for QuarantineMilter class."

View File

@@ -20,6 +20,8 @@ import logging.handlers
import sys import sys
import time import time
from email.header import decode_header, make_header
import pyquarantine import pyquarantine
from pyquarantine.version import __version__ as version from pyquarantine.version import __version__ as version
@@ -56,7 +58,7 @@ def print_table(columns, rows):
# get the length of the longest value # get the length of the longest value
lengths.append( lengths.append(
len(str(max(rows, key=lambda x: len(str(x[key])))[key]))) len(str(max(rows, key=lambda x: len(str(x[key])))[key])))
# use the the longer one # use the longer one
length = max(lengths) length = max(lengths)
column_lengths.append(length) column_lengths.append(length)
column_formats.append("{{:<{}}}".format(length)) column_formats.append("{{:<{}}}".format(length))
@@ -120,6 +122,9 @@ def list_quarantine_emails(config, args):
row["recipient"] = metadata["recipients"].pop(0) row["recipient"] = metadata["recipients"].pop(0)
if "subject" not in emails[quarantine_id]["headers"].keys(): if "subject" not in emails[quarantine_id]["headers"].keys():
emails[quarantine_id]["headers"]["subject"] = "" emails[quarantine_id]["headers"]["subject"] = ""
row["subject"] = str(make_header(decode_header(
emails[quarantine_id]["headers"]["subject"])))[:60].replace(
"\r", "").replace("\n", "").strip()
rows.append(row) rows.append(row)
if metadata["recipients"]: if metadata["recipients"]:

View File

@@ -19,10 +19,12 @@ import re
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from cgi import escape from cgi import escape
from collections import defaultdict from collections import defaultdict
from email.header import decode_header, make_header
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
from os.path import basename from os.path import basename
from urllib.parse import quote
from pyquarantine import mailer from pyquarantine import mailer
@@ -229,15 +231,12 @@ class EMailNotification(BaseNotification):
break break
if body is not None: if body is not None:
# get the character set, fallback to utf-8 if not defined in header charset = body.get_content_charset() or "utf-8"
charset = body.get_content_charset() content = body.get_payload(decode=True)
if charset is None: try:
charset = "utf-8" content = content.decode(encoding=charset, errors="replace")
except LookupError:
# decode content content = content.decode("utf-8", errors="replace")
content = body.get_payload(decode=True).decode(
encoding=charset, errors="replace")
content_type = body.get_content_type() content_type = body.get_content_type()
if content_type == EMailNotification._plain_text: if content_type == EMailNotification._plain_text:
# convert text/plain to text/html # convert text/plain to text/html
@@ -353,19 +352,26 @@ class EMailNotification(BaseNotification):
"{}: generating notification email for '{}'".format( "{}: generating notification email for '{}'".format(
queueid, recipient)) queueid, recipient))
self.logger.debug("{}: parsing email template".format(queueid)) self.logger.debug("{}: parsing email template".format(queueid))
if "from" not in headers.keys(): # decode some headers
headers["from"] = "" decoded_headers = {}
if "to" not in headers.keys(): for header in ["from", "to", "subject"]:
headers["to"] = "" if header in headers:
if "subject" not in headers.keys(): decoded_headers[header] = str(
headers["subject"] = "" make_header(decode_header(headers[header])))
else:
headers[header] = ""
decoded_headers[header] = ""
# generate dict containing all template variables # generate dict containing all template variables
variables = defaultdict(str, variables = defaultdict(str,
EMAIL_HTML_TEXT=sanitized_text, EMAIL_HTML_TEXT=sanitized_text,
EMAIL_FROM=escape(headers["from"]), EMAIL_FROM=escape(decoded_headers["from"]),
EMAIL_ENVELOPE_FROM=escape(mailfrom), EMAIL_ENVELOPE_FROM=escape(mailfrom),
EMAIL_TO=escape(recipient), EMAIL_ENVELOPE_FROM_URL=escape(quote(mailfrom)),
EMAIL_SUBJECT=escape(headers["subject"]), EMAIL_TO=escape(decoded_headers["to"]),
EMAIL_ENVELOPE_TO=escape(recipient),
EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)),
EMAIL_SUBJECT=escape(decoded_headers["subject"]),
EMAIL_QUARANTINE_ID=quarantine_id) EMAIL_QUARANTINE_ID=quarantine_id)
if subgroups: if subgroups:

View File

@@ -1 +1 @@
__version__ = "0.0.7" __version__ = "0.0.9"