improve handling of template variables

This commit is contained in:
2019-08-28 16:07:26 +02:00
parent 6ec70b834f
commit b41ae30335
7 changed files with 75 additions and 38 deletions

View File

@@ -22,6 +22,7 @@ import re
import sys
from Milter.utils import parse_addr
from collections import defaultdict
from io import BytesIO
from itertools import groupby
@@ -87,14 +88,11 @@ class QuarantineMilter(Milter.Base):
self.logger.debug("{}: received queue-id from MTA".format(self.queueid))
self.recipients = list(self.recipients)
self.headers = []
self.subject = ""
return Milter.CONTINUE
@Milter.noreply
def header(self, name, value):
self.headers.append("{}: {}".format(name, value))
if name.lower() == "subject":
self.subject = value
self.headers.append((name, value))
return Milter.CONTINUE
def eoh(self):
@@ -107,7 +105,8 @@ class QuarantineMilter(Milter.Base):
# iterate email headers
recipients_to_check = self.recipients.copy()
for header in self.headers:
for name, value in self.headers:
header = "{}: {}".format(name, value)
self.logger.debug("{}: checking header against configured quarantines: {}".format(self.queueid, header))
# iterate quarantines
for quarantine in self.config:
@@ -174,7 +173,9 @@ class QuarantineMilter(Milter.Base):
# initialize memory buffer to save email data
self.fp = BytesIO()
# write email headers to memory buffer
self.fp.write("{}\n".format("\n".join(self.headers)).encode())
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 quarantines, return configured action
quarantine = self._get_preferred_quarantine()
@@ -211,6 +212,9 @@ class QuarantineMilter(Milter.Base):
# iterate quarantines sorted by index
for quarantine, recipients in sorted(quarantines, key=lambda x: x[0]["index"]):
quarantine_id = ""
headers = defaultdict(str)
for name, value in self.headers:
headers[name.lower()] = value
subgroups = self.quarantines_matches[quarantine["name"]].groups(default="")
named_subgroups = self.quarantines_matches[quarantine["name"]].groupdict(default="")
@@ -219,7 +223,7 @@ class QuarantineMilter(Milter.Base):
# add email to quarantine
self.logger.info("{}: adding to quarantine '{}' for: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
try:
quarantine_id = quarantine["quarantine_obj"].add(self.queueid, self.mailfrom, recipients, self.subject, self.fp,
quarantine_id = quarantine["quarantine_obj"].add(self.queueid, self.mailfrom, recipients, headers, self.fp,
subgroups, named_subgroups)
except RuntimeError as e:
self.logger.error("{}: unable to add to quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
@@ -230,7 +234,7 @@ class QuarantineMilter(Milter.Base):
# notify
self.logger.info("{}: sending notification for quarantine '{}' to: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
try:
quarantine["notification_obj"].notify(self.queueid, quarantine_id, self.subject, self.mailfrom, recipients, self.fp,
quarantine["notification_obj"].notify(self.queueid, quarantine_id, self.mailfrom, recipients, headers, self.fp,
subgroups, named_subgroups)
except RuntimeError as e:
self.logger.error("{}: unable to send notification for quarantine '{}': {}".format(self.queueid, quarantine["name"], e))

View File

@@ -106,7 +106,7 @@ def list_quarantine_emails(config, args):
row["date"] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(metadata["date"]))
row["mailfrom"] = metadata["mailfrom"]
row["recipient"] = metadata["recipients"].pop(0)
row["subject"] = emails[quarantine_id]["subject"][:60]
row["subject"] = emails[quarantine_id]["headers"]["subject"][:60]
rows.append(row)
if metadata["recipients"]:

View File

@@ -18,6 +18,7 @@ import re
from bs4 import BeautifulSoup
from cgi import escape
from collections import defaultdict
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
@@ -33,7 +34,7 @@ class BaseNotification(object):
self.config = config
self.logger = logging.getLogger(__name__)
def notify(self, queueid, quarantine_id, subject, mailfrom, recipients, fp, subgroups=None, named_subgroups=None, synchronous=False):
def notify(self, queueid, quarantine_id, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None, synchronous=False):
fp.seek(0)
pass
@@ -106,7 +107,7 @@ class EMailNotification(BaseNotification):
super(EMailNotification, self).__init__(global_config, config, configtest)
# check if mandatory options are present in config
for option in ["smtp_host", "smtp_port", "notification_email_from", "notification_email_subject", "notification_email_template", "notification_email_replacement_img", "notification_email_embedded_imgs"]:
for option in ["smtp_host", "smtp_port", "notification_email_envelope_from", "notification_email_from", "notification_email_subject", "notification_email_template", "notification_email_replacement_img", "notification_email_embedded_imgs"]:
if option not in self.config.keys() and option in self.global_config.keys():
self.config[option] = self.global_config[option]
if option not in self.config.keys():
@@ -114,16 +115,31 @@ class EMailNotification(BaseNotification):
self.smtp_host = self.config["smtp_host"]
self.smtp_port = self.config["smtp_port"]
self.mailfrom = self.config["notification_email_from"]
self.mailfrom = self.config["notification_email_envelope_from"]
self.from_header = self.config["notification_email_from"]
self.subject = self.config["notification_email_subject"]
testvars = defaultdict(str, test="TEST")
# test-parse from header
try:
self.from_header.format_map(testvars)
except ValueError as e:
raise RuntimeError("error parsing notification_email_from: {}".format(e))
# test-parse subject
try:
self.subject.format_map(testvars)
except ValueError as e:
raise RuntimeError("error parsing notification_email_subject: {}".format(e))
# read and parse email notification template
try:
self.template = open(self.config["notification_email_template"], "r").read()
self.template.format(TEST="test")
self.template.format_map(testvars)
except IOError as e:
raise RuntimeError("error reading template: {}".format(e))
except KeyError as e:
except ValueError as e:
raise RuntimeError("error parsing template: {}".format(e))
# read email replacement image if specified
@@ -227,9 +243,9 @@ class EMailNotification(BaseNotification):
return soup
def notify(self, queueid, quarantine_id, subject, mailfrom, recipients, fp, subgroups=None, named_subgroups=None, synchronous=False):
def notify(self, queueid, quarantine_id, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None, synchronous=False):
"Notify recipients via email."
super(EMailNotification, self).notify(queueid, quarantine_id, subject, mailfrom, recipients, fp, subgroups, named_subgroups, synchronous)
super(EMailNotification, self).notify(queueid, quarantine_id, mailfrom, recipients, headers, fp, subgroups, named_subgroups, synchronous)
# extract html text from email
self.logger.debug("{}: extraction email text from original email".format(queueid))
@@ -246,6 +262,7 @@ class EMailNotification(BaseNotification):
# sanitizing email text of original email
sanitized_text = self.sanitize(queueid, soup)
del soup
# sending email notifications
for recipient in recipients:
@@ -253,13 +270,14 @@ class EMailNotification(BaseNotification):
self.logger.debug("{}: parsing email template".format(queueid))
# generate dict containing all template variables
variables = {
"EMAIL_HTML_TEXT": sanitized_text,
"EMAIL_FROM": escape(mailfrom),
"EMAIL_TO": escape(recipient),
"EMAIL_SUBJECT": escape(subject),
"EMAIL_QUARANTINE_ID": quarantine_id
}
variables = defaultdict(str,
EMAIL_HTML_TEXT=sanitized_text,
EMAIL_FROM=escape(headers["from"]),
EMAIL_ENVELOPE_FROM=escape(mailfrom),
EMAIL_TO=escape(recipient),
EMAIL_SUBJECT=escape(headers["subject"]),
EMAIL_QUARANTINE_ID=quarantine_id)
if subgroups:
number = 0
for subgroup in subgroups:
@@ -269,11 +287,11 @@ class EMailNotification(BaseNotification):
variables.update(named_subgroups)
# parse template
htmltext = self.template.format(**variables)
htmltext = self.template.format_map(variables)
msg = MIMEMultipart('alternative')
msg["Subject"] = self.subject
msg["From"] = "<{}>".format(self.mailfrom)
msg["Subject"] = self.subject.format_map(variables)
msg["From"] = "<{}>".format(self.from_header.format_map(variables))
msg["To"] = "<{}>".format(recipient)
msg["Date"] = email.utils.formatdate()
msg.attach(MIMEText(htmltext, "html", 'UTF-8'))

View File

@@ -33,7 +33,7 @@ class BaseQuarantine(object):
self.config = config
self.logger = logging.getLogger(__name__)
def add(self, queueid, mailfrom, recipients, subject, fp, subgroups=None, named_subgroups=None):
def add(self, queueid, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None):
"Add email to quarantine."
fp.seek(0)
return ""
@@ -109,9 +109,9 @@ class FileQuarantine(BaseQuarantine):
except IOError as e:
raise RuntimeError("unable to remove data file: {}".format(e))
def add(self, queueid, mailfrom, recipients, subject, fp, subgroups=None, named_subgroups=None):
def add(self, queueid, mailfrom, recipients, headers, fp, subgroups=None, named_subgroups=None):
"Add email to file quarantine and return quarantine-id."
super(FileQuarantine, self).add(queueid, mailfrom, recipients, subject, fp, subgroups, named_subgroups)
super(FileQuarantine, self).add(queueid, mailfrom, recipients, headers, fp, subgroups, named_subgroups)
quarantine_id = "{}_{}".format(datetime.now().strftime("%Y%m%d%H%M%S"), queueid)
# save mail
@@ -121,7 +121,7 @@ class FileQuarantine(BaseQuarantine):
metadata = {
"mailfrom": mailfrom,
"recipients": recipients,
"subject": subject,
"headers": headers,
"date": timegm(gmtime()),
"queue_id": queueid,
"subgroups": subgroups,
@@ -225,7 +225,7 @@ class FileQuarantine(BaseQuarantine):
datafile = os.path.join(self.directory, quarantine_id)
try:
with open(datafile, "rb") as fp:
self.config["notification_obj"].notify(metadata["queue_id"], quarantine_id, metadata["subject"], metadata["mailfrom"], recipients, fp,
self.config["notification_obj"].notify(metadata["queue_id"], quarantine_id, metadata["mailfrom"], recipients, metadata["headers"], fp,
metadata["subgroups"], metadata["named_subgroups"], synchronous=True)
except IOError as e:
raise(RuntimeError("unable to read data file: {}".format(e)))