From f37b50eaac590510b7a60a50de171a17104dd81a Mon Sep 17 00:00:00 2001 From: Thomas Oettli Date: Wed, 1 May 2019 13:02:41 +0200 Subject: [PATCH] Add dynamic template variables according to regex --- README.md | 7 +++++++ pyquarantine/__init__.py | 16 +++++++++++----- pyquarantine/notifications.py | 34 +++++++++++++++++++++------------- pyquarantine/quarantines.py | 13 ++++++++----- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index cedb580..e6303e3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,13 @@ The following configuration options are mandatory in each quarantine section: * **{EMAIL_HTML_TEXT}** Sanitized version of the e-mail text part of the original e-mail. Only harmless HTML tags and attributes are included. Images are replaced with the image set by notification_email_replacement_img option. + Some template variables are only available if the regex of the matching quarantine contains subgroups or named subgroups (python syntax). This is useful to include information (e.g. virus names, spam points, ...) of the matching header within the notification. + The following dynamic tempalte variables are available: + * **{SUBGROUP_n}** + Content of a subgroup, 'n' will be replaced by the index number of each subgroup, starting with 0. + * **{subgroup_name}** + Content of a named subgroup, 'subgroup_name' will be replaced by its name. + The following configuration options are mandatory for this notification type: * **notification_email_from** Notification e-mail from-address. diff --git a/pyquarantine/__init__.py b/pyquarantine/__init__.py index 88e5e42..1363fa9 100644 --- a/pyquarantine/__init__.py +++ b/pyquarantine/__init__.py @@ -119,9 +119,13 @@ class QuarantineMilter(Milter.Base): # check email header against quarantine regex self.logger.debug("{}: {}: checking header against regex '{}'".format(self.queueid, quarantine["name"], quarantine["regex"])) - if quarantine["regex_compiled"].match(header): + match = quarantine["regex_compiled"].search(header) + if match: self.logger.debug("{}: {}: header matched regex".format(self.queueid, quarantine["name"])) - + if "subgroups" not in quarantine.keys(): + # save subgroups of match into the quarantine object for later use as template variables + quarantine["subgroups"] = match.groups(default="") + quarantine["named_subgroups"] = match.groupdict(default="") # check for whitelisted recipients whitelist = quarantine["whitelist_obj"] if whitelist != None: @@ -212,7 +216,8 @@ 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, fp=self.fp) + quarantine_id = quarantine["quarantine_obj"].add(self.queueid, self.mailfrom, recipients, self.subject, self.fp, + quarantine["subgroups"], quarantine["named_subgroups"]) except RuntimeError as e: self.logger.error("{}: unable to add to quarantine '{}': {}".format(self.queueid, quarantine["name"], e)) return Milter.TEMPFAIL @@ -222,7 +227,8 @@ 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, fp=self.fp) + quarantine["notification_obj"].notify(self.queueid, quarantine_id, self.subject, self.mailfrom, recipients, self.fp, + quarantine["subgroups"], quarantine["named_subgroups"]) except RuntimeError as e: self.logger.error("{}: unable to send notification for quarantine '{}': {}".format(self.queueid, quarantine["name"], e)) return Milter.TEMPFAIL @@ -316,7 +322,7 @@ def generate_milter_config(configtest=False, config_files=[]): # pre-compile regex logger.debug("{}: compiling regex '{}'".format(quarantine_name, config["regex"])) - config["regex_compiled"] = re.compile(config["regex"]) + config["regex_compiled"] = re.compile(config["regex"], re.MULTILINE + re.DOTALL) # create quarantine instance quarantine_type = config["quarantine_type"].lower() diff --git a/pyquarantine/notifications.py b/pyquarantine/notifications.py index 8012120..1d7066d 100644 --- a/pyquarantine/notifications.py +++ b/pyquarantine/notifications.py @@ -33,7 +33,7 @@ class BaseNotification(object): self.config = config self.logger = logging.getLogger(__name__) - def notify(self, queueid, quarantine_id, subject, mailfrom, recipients, fp, synchronous=False): + def notify(self, queueid, quarantine_id, subject, mailfrom, recipients, fp, subgroups=None, named_subgroups=None, synchronous=False): fp.seek(0) pass @@ -224,9 +224,9 @@ class EMailNotification(BaseNotification): return soup - def notify(self, queueid, quarantine_id, subject, mailfrom, recipients, fp, synchronous=False): + def notify(self, queueid, quarantine_id, subject, mailfrom, recipients, fp, subgroups=None, named_subgroups=None, synchronous=False): "Notify recipients via email." - super(EMailNotification, self).notify(queueid, quarantine_id, subject, mailfrom, recipients, fp, synchronous) + super(EMailNotification, self).notify(queueid, quarantine_id, subject, mailfrom, recipients, fp, subgroups, named_subgroups, synchronous) # extract html text from email self.logger.debug("{}: extraction email text from original email".format(queueid)) @@ -244,21 +244,29 @@ class EMailNotification(BaseNotification): # sanitizing email text of original email sanitized_text = self.sanitize(queueid, soup) - # escape possible html entities in subject - subject = escape(subject) - # sending email notifications for recipient in recipients: self.logger.debug("{}: generating notification email for '{}'".format(queueid, recipient)) self.logger.debug("{}: parsing email template".format(queueid)) - htmltext = self.template.format( \ - EMAIL_HTML_TEXT=sanitized_text, \ - EMAIL_FROM=escape(mailfrom), \ - EMAIL_TO=escape(recipient), \ - EMAIL_SUBJECT=subject, \ - EMAIL_QUARANTINE_ID=quarantine_id - ) + # 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 + } + if subgroups: + number = 0 + for subgroup in subgroups: + variables["SUBGROUP_{}".format(number)] = escape(subgroup) + if named_subgroups: + for key, value in named_subgroups.items(): named_subgroups[key] = escape(value) + variables.update(named_subgroups) + + # parse template + htmltext = self.template.format(**variables) msg = MIMEMultipart('alternative') msg["Subject"] = self.subject diff --git a/pyquarantine/quarantines.py b/pyquarantine/quarantines.py index 4e99f74..c05941a 100644 --- a/pyquarantine/quarantines.py +++ b/pyquarantine/quarantines.py @@ -33,7 +33,7 @@ class BaseQuarantine(object): self.config = config self.logger = logging.getLogger(__name__) - def add(self, queueid, mailfrom, recipients, subject, fp): + def add(self, queueid, mailfrom, recipients, subject, 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): + def add(self, queueid, mailfrom, recipients, subject, fp, subgroups=None, named_subgroups=None): "Add email to file quarantine and return quarantine-id." - super(FileQuarantine, self).add(queueid, mailfrom, recipients, subject, fp) + super(FileQuarantine, self).add(queueid, mailfrom, recipients, subject, fp, subgroups, named_subgroups) quarantine_id = "{}_{}".format(datetime.now().strftime("%Y%m%d%H%M%S"), queueid) # save mail @@ -123,7 +123,9 @@ class FileQuarantine(BaseQuarantine): "recipients": recipients, "subject": subject, "date": timegm(gmtime()), - "queue_id": queueid + "queue_id": queueid, + "subgroups": subgroups, + "named_subgroups": named_subgroups } try: self._save_metafile(quarantine_id, metadata) @@ -223,7 +225,8 @@ 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, synchronous=True) + self.config["notification_obj"].notify(metadata["queue_id"], quarantine_id, metadata["subject"], metadata["mailfrom"], recipients, fp, + metadata["subgroups"], metadata["named_subgroups"], synchronous=True) except IOError as e: raise(RuntimeError("unable to read data file: {}".format(e)))