12 Commits
0.0.5 ... 0.0.9

6 changed files with 86 additions and 49 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,38 +257,24 @@ 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: # quarantine and notification are disabled on all matching
self.logger.debug( # quarantines, just return configured action
"{}: initializing memory buffer to save email data".format( quarantine = self._get_preferred_quarantine()
self.queueid)) self.logger.info(
# initialize memory buffer to save email data "{}: {} matching quarantine is '{}', performing milter action {}".format(
self.fp = BytesIO() self.queueid,
# write email headers to memory buffer self.global_config["preferred_quarantine_action"],
for name, value in self.headers: quarantine["name"],
self.fp.write("{}: {}\n".format(name, value).encode()) quarantine["action"].upper()))
self.fp.write("\n".encode()) if quarantine["action"] == "reject":
else: self.setreply("554", "5.7.0", quarantine["reject_reason"])
# quarantine and notification are disabled on all matching return quarantine["milter_action"]
# quarantines, return configured action
quarantine = self._get_preferred_quarantine()
self.logger.info(
"{}: {} matching quarantine is '{}', performing milter action {}".format(
self.queueid,
self.global_config["preferred_quarantine_action"],
quarantine["name"],
quarantine["action"].upper()))
if quarantine["action"] == "reject":
self.setreply("554", "5.7.0", quarantine["reject_reason"])
return quarantine["milter_action"]
return Milter.CONTINUE
except Exception as e: except Exception as e:
self.logger.exception( self.logger.exception(
@@ -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))
@@ -118,7 +120,11 @@ def list_quarantine_emails(config, args):
metadata["date"])) metadata["date"]))
row["mailfrom"] = metadata["mailfrom"] row["mailfrom"] = metadata["mailfrom"]
row["recipient"] = metadata["recipients"].pop(0) row["recipient"] = metadata["recipients"].pop(0)
row["subject"] = emails[quarantine_id]["headers"]["subject"][:60] if "subject" not in emails[quarantine_id]["headers"].keys():
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,14 +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))
# decode some headers
decoded_headers = {}
for header in ["from", "to", "subject"]:
if header in headers:
decoded_headers[header] = str(
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.5" __version__ = "0.0.9"