From 9e72c0098386e2deb5570445b1e86b533a3f8684 Mon Sep 17 00:00:00 2001 From: Thomas Oettli Date: Fri, 15 Mar 2019 17:23:13 +0100 Subject: [PATCH] Ability to embed custom images in notifications --- README.md | 10 ++++--- docs/pyquarantine.conf.example | 14 +++++++-- pyquarantine/notifications.py | 52 +++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 47c6c6a..b6dfacf 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The project is currently in alpha status, but will soon be used in a productive * BeautifulSoup ## Configuration -The pyquarantine module uses an INI-style configuration file. The sections are described below. +The pyquarantine module uses an INI-style configuration file. The sections are described below. If you have to specify a path in the config, you can always use a relative path to the last loaded config file. ### Section "global" Any available configuration option can be set in the global section as default instead of in a quarantine section. @@ -85,10 +85,12 @@ The following configuration options are mandatory in each quarantine section: * **notification_email_subject** Notification e-mail subject. * **notification_email_template** - Notification e-mail template to use. + Path to the notification e-mail template. It is hold in memory during runtime. * **notification_email_replacement_img** - An image to replace images in e-mail. - + Path to the image to replace images in e-mails. It is hold in memory during runtime. Leave it empty to disable. + * **notification_email_embedded_imgs** + Comma-separated list of images to attach to the notification e-mail. The Content-ID of each image will be set to the filename, so you can reference it from the e-mail template. All images are hold in memory during runtime. + Leave empty to disable. ### Actions Every quarantine responds with a milter-action if an e-mail header matches the configured regular expression. diff --git a/docs/pyquarantine.conf.example b/docs/pyquarantine.conf.example index a66334e..26f42bb 100644 --- a/docs/pyquarantine.conf.example +++ b/docs/pyquarantine.conf.example @@ -94,18 +94,26 @@ notification_email_subject = Spam Quarantine Notification # Notes: Set the template used when sending notification emails. # A relative path to this config file can be used. # This option is needed by notification type 'email'. -# Values: [ TEMPLATE_FILE ] +# Values: [ TEMPLATE_PATH ] # notification_email_template = templates/notification.template # Option: notification_email_replacement_img -# Notes: Set the replacement image for images within emails. +# Notes: Set the path to the replacement image for img tags within emails. # A relative path to this config file can be used. # This option is needed by notification type 'email'. -# Values: [ IMAGE_FILE ] +# Values: [ IMAGE_PATH ] # notification_email_replacement_img = templates/removed.png +# Option: notification_email_embedded_imgs +# Notes: Set a list of paths to images to embed in e-mails (comma-separated). +# Relative paths to this config file can be used. +# This option is needed by notification type 'email'. +# Values: [ IMAGE_PATH ] +# +notification_email_embedded_imgs = templates/logo.png + # Option: whitelist_type # Notes: Set the whitelist type. # Values: [ db | none ] diff --git a/pyquarantine/notifications.py b/pyquarantine/notifications.py index 09f8e82..8012120 100644 --- a/pyquarantine/notifications.py +++ b/pyquarantine/notifications.py @@ -21,6 +21,7 @@ from cgi import escape from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage +from os.path import basename from pyquarantine import mailer @@ -105,7 +106,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"]: + for option in ["smtp_host", "smtp_port", "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(): @@ -122,13 +123,31 @@ class EMailNotification(BaseNotification): except IOError as e: raise RuntimeError("error reading template: {}".format(e)) - # read email replacement image - try: - self.replacement_img = MIMEImage(open(self.config["notification_email_replacement_img"], "rb").read()) - except IOError as e: - raise RuntimeError("error reading replacement image: {}".format(e)) + # read email replacement image if specified + replacement_img_path = self.config["notification_email_replacement_img"].strip() + if replacement_img_path: + try: + self.replacement_img = MIMEImage(open(replacement_img_path, "rb").read()) + except IOError as e: + raise RuntimeError("error reading replacement image: {}".format(e)) + else: + self.replacement_img.add_header("Content-ID", "") else: - self.replacement_img.add_header("Content-ID", "") + self.replacement_img = None + + # read images to embed if specified + embedded_img_paths = [ p.strip() for p in self.config["notification_email_embedded_imgs"].split(",") if p] + self.embedded_imgs = [] + for img_path in embedded_img_paths: + # read image + try: + img = MIMEImage(open(img_path, "rb").read()) + except IOError as e: + raise RuntimeError("error reading image: {}".format(e)) + else: + img.add_header("Content-ID", "<{}>".format(basename(img_path))) + self.embedded_imgs.append(img) + def get_text(self, queueid, part): "Get the mail text in html form from email part." @@ -214,12 +233,13 @@ class EMailNotification(BaseNotification): soup = self.get_html_text_part(queueid, email.message_from_binary_file(fp)) # replace picture sources - picture_replaced = False - for element in soup("img"): - if "src" in element.attrs.keys(): - self.logger.debug("{}: replacing image: {}".format(queueid, element["src"])) - element["src"] = "cid:removed_for_security_reasons" - picture_replaced = True + image_replaced = False + if self.replacement_img: + for element in soup("img"): + if "src" in element.attrs.keys(): + self.logger.debug("{}: replacing image: {}".format(queueid, element["src"])) + element["src"] = "cid:removed_for_security_reasons" + image_replaced = True # sanitizing email text of original email sanitized_text = self.sanitize(queueid, soup) @@ -247,10 +267,14 @@ class EMailNotification(BaseNotification): msg["Date"] = email.utils.formatdate() msg.attach(MIMEText(htmltext, "html", 'UTF-8')) - if picture_replaced: + if image_replaced: self.logger.debug("{}: attaching notification_replacement_img".format(queueid)) msg.attach(self.replacement_img) + for img in self.embedded_imgs: + self.logger.debug("{}: attaching imgage".format(queueid)) + msg.attach(img) + self.logger.debug("{}: sending notification email to: {}".format(queueid, recipient)) if synchronous: try: