improve handling of template variables
This commit is contained in:
10
README.md
10
README.md
@@ -73,8 +73,10 @@ The following configuration options are optional in each quarantine section:
|
|||||||
Quarantine e-mail notifications are sent to recipients. The SMTP host and port, E-mail template, from-address and the subject are configurable for each quarantine. The templates must contain the notification e-mail text in HTML form.
|
Quarantine e-mail notifications are sent to recipients. The SMTP host and port, E-mail template, from-address and the subject are configurable for each quarantine. The templates must contain the notification e-mail text in HTML form.
|
||||||
|
|
||||||
The following template variables are available:
|
The following template variables are available:
|
||||||
|
* **{EMAIL_ENVELOPE_FROM}**
|
||||||
|
E-mail from-address received by the milter.
|
||||||
* **{EMAIL_FROM}**
|
* **{EMAIL_FROM}**
|
||||||
E-mail from-address received by the milter (envelope-from).
|
Value of the from header of the original e-mail.
|
||||||
* **{EMAIL_TO}**
|
* **{EMAIL_TO}**
|
||||||
E-mail recipient address of this notification.
|
E-mail recipient address of this notification.
|
||||||
* **{EMAIL_SUBJECT}**
|
* **{EMAIL_SUBJECT}**
|
||||||
@@ -92,10 +94,12 @@ The following configuration options are optional in each quarantine section:
|
|||||||
Content of a named subgroup, 'subgroup_name' will be replaced by its name.
|
Content of a named subgroup, 'subgroup_name' will be replaced by its name.
|
||||||
|
|
||||||
The following configuration options are mandatory for this notification type:
|
The following configuration options are mandatory for this notification type:
|
||||||
|
* **notification_email_envelope_from**
|
||||||
|
Notification e-mail envelope from-address.
|
||||||
* **notification_email_from**
|
* **notification_email_from**
|
||||||
Notification e-mail from-address.
|
Value of the notification e-mail from header. Every e-mail template variable described above is usable.
|
||||||
* **notification_email_subject**
|
* **notification_email_subject**
|
||||||
Notification e-mail subject.
|
Notification e-mail subject. All e-mail template variable described above is usable.
|
||||||
* **notification_email_template**
|
* **notification_email_template**
|
||||||
Path to the notification e-mail template. It is hold in memory during runtime.
|
Path to the notification e-mail template. It is hold in memory during runtime.
|
||||||
* **notification_email_replacement_img**
|
* **notification_email_replacement_img**
|
||||||
|
|||||||
@@ -82,12 +82,19 @@ reject_reason = Message rejected
|
|||||||
#
|
#
|
||||||
notification_type = email
|
notification_type = email
|
||||||
|
|
||||||
# Option: notification_email_from
|
# Option: notification_email_envelope_from
|
||||||
# Notes: Set the from address used when sending notification emails.
|
# Notes: Set the envelope-from address used when sending notification emails.
|
||||||
# This option is needed by notification type 'email'.
|
# This option is needed by notification type 'email'.
|
||||||
# Values: [ FROM_ADDRESS ]
|
# Values: [ ENVELOPE_FROM_ADDRESS ]
|
||||||
#
|
#
|
||||||
notification_email_from = notification@domain.tld
|
notification_email_envelope_from = notification@domain.tld
|
||||||
|
|
||||||
|
# Option: notification_email_from
|
||||||
|
# Notes: Set the from header used when sending notification emails.
|
||||||
|
# This option is needed by notification type 'email'.
|
||||||
|
# Values: [ FROM_HEADER ]
|
||||||
|
#
|
||||||
|
notification_email_from = Notification <notification@domain.tld>
|
||||||
|
|
||||||
# Option: notification_email_usbject
|
# Option: notification_email_usbject
|
||||||
# Notes: Set the subject used when sending notification emails.
|
# Notes: Set the subject used when sending notification emails.
|
||||||
|
|||||||
4
docs/templates/notification.template
vendored
4
docs/templates/notification.template
vendored
@@ -2,6 +2,10 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>Quarantine notification</h1>
|
<h1>Quarantine notification</h1>
|
||||||
<table>
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><b>Envelope-From:</b></td>
|
||||||
|
<td>{EMAIL_ENVELOPE_FROM}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>From:</b></td>
|
<td><b>From:</b></td>
|
||||||
<td>{EMAIL_FROM}</td>
|
<td>{EMAIL_FROM}</td>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from Milter.utils import parse_addr
|
from Milter.utils import parse_addr
|
||||||
|
from collections import defaultdict
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
@@ -87,14 +88,11 @@ class QuarantineMilter(Milter.Base):
|
|||||||
self.logger.debug("{}: received queue-id from MTA".format(self.queueid))
|
self.logger.debug("{}: received queue-id from MTA".format(self.queueid))
|
||||||
self.recipients = list(self.recipients)
|
self.recipients = list(self.recipients)
|
||||||
self.headers = []
|
self.headers = []
|
||||||
self.subject = ""
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@Milter.noreply
|
@Milter.noreply
|
||||||
def header(self, name, value):
|
def header(self, name, value):
|
||||||
self.headers.append("{}: {}".format(name, value))
|
self.headers.append((name, value))
|
||||||
if name.lower() == "subject":
|
|
||||||
self.subject = value
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def eoh(self):
|
def eoh(self):
|
||||||
@@ -107,7 +105,8 @@ class QuarantineMilter(Milter.Base):
|
|||||||
|
|
||||||
# iterate email headers
|
# iterate email headers
|
||||||
recipients_to_check = self.recipients.copy()
|
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))
|
self.logger.debug("{}: checking header against configured quarantines: {}".format(self.queueid, header))
|
||||||
# iterate quarantines
|
# iterate quarantines
|
||||||
for quarantine in self.config:
|
for quarantine in self.config:
|
||||||
@@ -174,7 +173,9 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# initialize memory buffer to save email data
|
# initialize memory buffer to save email data
|
||||||
self.fp = BytesIO()
|
self.fp = BytesIO()
|
||||||
# write email headers to memory buffer
|
# 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:
|
else:
|
||||||
# quarantine and notification are disabled on all matching quarantines, return configured action
|
# quarantine and notification are disabled on all matching quarantines, return configured action
|
||||||
quarantine = self._get_preferred_quarantine()
|
quarantine = self._get_preferred_quarantine()
|
||||||
@@ -211,6 +212,9 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# iterate quarantines sorted by index
|
# iterate quarantines sorted by index
|
||||||
for quarantine, recipients in sorted(quarantines, key=lambda x: x[0]["index"]):
|
for quarantine, recipients in sorted(quarantines, key=lambda x: x[0]["index"]):
|
||||||
quarantine_id = ""
|
quarantine_id = ""
|
||||||
|
headers = defaultdict(str)
|
||||||
|
for name, value in self.headers:
|
||||||
|
headers[name.lower()] = value
|
||||||
subgroups = self.quarantines_matches[quarantine["name"]].groups(default="")
|
subgroups = self.quarantines_matches[quarantine["name"]].groups(default="")
|
||||||
named_subgroups = self.quarantines_matches[quarantine["name"]].groupdict(default="")
|
named_subgroups = self.quarantines_matches[quarantine["name"]].groupdict(default="")
|
||||||
|
|
||||||
@@ -219,7 +223,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# add email to quarantine
|
# add email to quarantine
|
||||||
self.logger.info("{}: adding to quarantine '{}' for: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
|
self.logger.info("{}: adding to quarantine '{}' for: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
|
||||||
try:
|
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)
|
subgroups, named_subgroups)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
self.logger.error("{}: unable to add to quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
|
self.logger.error("{}: unable to add to quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
|
||||||
@@ -230,7 +234,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# notify
|
# notify
|
||||||
self.logger.info("{}: sending notification for quarantine '{}' to: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
|
self.logger.info("{}: sending notification for quarantine '{}' to: {}".format(self.queueid, quarantine["name"], ", ".join(recipients)))
|
||||||
try:
|
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)
|
subgroups, named_subgroups)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
self.logger.error("{}: unable to send notification for quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
|
self.logger.error("{}: unable to send notification for quarantine '{}': {}".format(self.queueid, quarantine["name"], e))
|
||||||
|
|||||||
@@ -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["date"] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(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]["subject"][:60]
|
row["subject"] = emails[quarantine_id]["headers"]["subject"][:60]
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
if metadata["recipients"]:
|
if metadata["recipients"]:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import re
|
|||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from cgi import escape
|
from cgi import escape
|
||||||
|
from collections import defaultdict
|
||||||
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
|
||||||
@@ -33,7 +34,7 @@ class BaseNotification(object):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.logger = logging.getLogger(__name__)
|
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)
|
fp.seek(0)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ class EMailNotification(BaseNotification):
|
|||||||
super(EMailNotification, self).__init__(global_config, config, configtest)
|
super(EMailNotification, self).__init__(global_config, config, configtest)
|
||||||
|
|
||||||
# check if mandatory options are present in config
|
# 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():
|
if option not in self.config.keys() and option in self.global_config.keys():
|
||||||
self.config[option] = self.global_config[option]
|
self.config[option] = self.global_config[option]
|
||||||
if option not in self.config.keys():
|
if option not in self.config.keys():
|
||||||
@@ -114,16 +115,31 @@ class EMailNotification(BaseNotification):
|
|||||||
|
|
||||||
self.smtp_host = self.config["smtp_host"]
|
self.smtp_host = self.config["smtp_host"]
|
||||||
self.smtp_port = self.config["smtp_port"]
|
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"]
|
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
|
# read and parse email notification template
|
||||||
try:
|
try:
|
||||||
self.template = open(self.config["notification_email_template"], "r").read()
|
self.template = open(self.config["notification_email_template"], "r").read()
|
||||||
self.template.format(TEST="test")
|
self.template.format_map(testvars)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise RuntimeError("error reading template: {}".format(e))
|
raise RuntimeError("error reading template: {}".format(e))
|
||||||
except KeyError as e:
|
except ValueError as e:
|
||||||
raise RuntimeError("error parsing template: {}".format(e))
|
raise RuntimeError("error parsing template: {}".format(e))
|
||||||
|
|
||||||
# read email replacement image if specified
|
# read email replacement image if specified
|
||||||
@@ -227,9 +243,9 @@ class EMailNotification(BaseNotification):
|
|||||||
|
|
||||||
return soup
|
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."
|
"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
|
# extract html text from email
|
||||||
self.logger.debug("{}: extraction email text from original email".format(queueid))
|
self.logger.debug("{}: extraction email text from original email".format(queueid))
|
||||||
@@ -246,6 +262,7 @@ class EMailNotification(BaseNotification):
|
|||||||
|
|
||||||
# sanitizing email text of original email
|
# sanitizing email text of original email
|
||||||
sanitized_text = self.sanitize(queueid, soup)
|
sanitized_text = self.sanitize(queueid, soup)
|
||||||
|
del soup
|
||||||
|
|
||||||
# sending email notifications
|
# sending email notifications
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
@@ -253,13 +270,14 @@ class EMailNotification(BaseNotification):
|
|||||||
self.logger.debug("{}: parsing email template".format(queueid))
|
self.logger.debug("{}: parsing email template".format(queueid))
|
||||||
|
|
||||||
# generate dict containing all template variables
|
# generate dict containing all template variables
|
||||||
variables = {
|
variables = defaultdict(str,
|
||||||
"EMAIL_HTML_TEXT": sanitized_text,
|
EMAIL_HTML_TEXT=sanitized_text,
|
||||||
"EMAIL_FROM": escape(mailfrom),
|
EMAIL_FROM=escape(headers["from"]),
|
||||||
"EMAIL_TO": escape(recipient),
|
EMAIL_ENVELOPE_FROM=escape(mailfrom),
|
||||||
"EMAIL_SUBJECT": escape(subject),
|
EMAIL_TO=escape(recipient),
|
||||||
"EMAIL_QUARANTINE_ID": quarantine_id
|
EMAIL_SUBJECT=escape(headers["subject"]),
|
||||||
}
|
EMAIL_QUARANTINE_ID=quarantine_id)
|
||||||
|
|
||||||
if subgroups:
|
if subgroups:
|
||||||
number = 0
|
number = 0
|
||||||
for subgroup in subgroups:
|
for subgroup in subgroups:
|
||||||
@@ -269,11 +287,11 @@ class EMailNotification(BaseNotification):
|
|||||||
variables.update(named_subgroups)
|
variables.update(named_subgroups)
|
||||||
|
|
||||||
# parse template
|
# parse template
|
||||||
htmltext = self.template.format(**variables)
|
htmltext = self.template.format_map(variables)
|
||||||
|
|
||||||
msg = MIMEMultipart('alternative')
|
msg = MIMEMultipart('alternative')
|
||||||
msg["Subject"] = self.subject
|
msg["Subject"] = self.subject.format_map(variables)
|
||||||
msg["From"] = "<{}>".format(self.mailfrom)
|
msg["From"] = "<{}>".format(self.from_header.format_map(variables))
|
||||||
msg["To"] = "<{}>".format(recipient)
|
msg["To"] = "<{}>".format(recipient)
|
||||||
msg["Date"] = email.utils.formatdate()
|
msg["Date"] = email.utils.formatdate()
|
||||||
msg.attach(MIMEText(htmltext, "html", 'UTF-8'))
|
msg.attach(MIMEText(htmltext, "html", 'UTF-8'))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class BaseQuarantine(object):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.logger = logging.getLogger(__name__)
|
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."
|
"Add email to quarantine."
|
||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
return ""
|
return ""
|
||||||
@@ -109,9 +109,9 @@ class FileQuarantine(BaseQuarantine):
|
|||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise RuntimeError("unable to remove data file: {}".format(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."
|
"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)
|
quarantine_id = "{}_{}".format(datetime.now().strftime("%Y%m%d%H%M%S"), queueid)
|
||||||
|
|
||||||
# save mail
|
# save mail
|
||||||
@@ -121,7 +121,7 @@ class FileQuarantine(BaseQuarantine):
|
|||||||
metadata = {
|
metadata = {
|
||||||
"mailfrom": mailfrom,
|
"mailfrom": mailfrom,
|
||||||
"recipients": recipients,
|
"recipients": recipients,
|
||||||
"subject": subject,
|
"headers": headers,
|
||||||
"date": timegm(gmtime()),
|
"date": timegm(gmtime()),
|
||||||
"queue_id": queueid,
|
"queue_id": queueid,
|
||||||
"subgroups": subgroups,
|
"subgroups": subgroups,
|
||||||
@@ -225,7 +225,7 @@ class FileQuarantine(BaseQuarantine):
|
|||||||
datafile = os.path.join(self.directory, quarantine_id)
|
datafile = os.path.join(self.directory, quarantine_id)
|
||||||
try:
|
try:
|
||||||
with open(datafile, "rb") as fp:
|
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)
|
metadata["subgroups"], metadata["named_subgroups"], synchronous=True)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise(RuntimeError("unable to read data file: {}".format(e)))
|
raise(RuntimeError("unable to read data file: {}".format(e)))
|
||||||
|
|||||||
Reference in New Issue
Block a user