Ability to embed custom images in notifications

This commit is contained in:
2019-03-15 17:23:13 +01:00
parent 7368bab1b1
commit 9e72c00983
3 changed files with 55 additions and 21 deletions

View File

@@ -16,7 +16,7 @@ The project is currently in alpha status, but will soon be used in a productive
* BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/> * BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/>
## Configuration ## 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" ### Section "global"
Any available configuration option can be set in the global section as default instead of in a quarantine section. 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_email_subject**
Notification e-mail subject. Notification e-mail subject.
* **notification_email_template** * **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** * **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 ### Actions
Every quarantine responds with a milter-action if an e-mail header matches the configured regular expression. Every quarantine responds with a milter-action if an e-mail header matches the configured regular expression.

View File

@@ -94,18 +94,26 @@ notification_email_subject = Spam Quarantine Notification
# Notes: Set the template used when sending notification emails. # Notes: Set the template used when sending notification emails.
# A relative path to this config file can be used. # A relative path to this config file can be used.
# This option is needed by notification type 'email'. # This option is needed by notification type 'email'.
# Values: [ TEMPLATE_FILE ] # Values: [ TEMPLATE_PATH ]
# #
notification_email_template = templates/notification.template notification_email_template = templates/notification.template
# Option: notification_email_replacement_img # 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. # A relative path to this config file can be used.
# This option is needed by notification type 'email'. # This option is needed by notification type 'email'.
# Values: [ IMAGE_FILE ] # Values: [ IMAGE_PATH ]
# #
notification_email_replacement_img = templates/removed.png 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 # Option: whitelist_type
# Notes: Set the whitelist type. # Notes: Set the whitelist type.
# Values: [ db | none ] # Values: [ db | none ]

View File

@@ -21,6 +21,7 @@ from cgi import escape
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 pyquarantine import mailer from pyquarantine import mailer
@@ -105,7 +106,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"]: 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(): 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():
@@ -122,13 +123,31 @@ class EMailNotification(BaseNotification):
except IOError as e: except IOError as e:
raise RuntimeError("error reading template: {}".format(e)) raise RuntimeError("error reading template: {}".format(e))
# read email replacement image # read email replacement image if specified
replacement_img_path = self.config["notification_email_replacement_img"].strip()
if replacement_img_path:
try: try:
self.replacement_img = MIMEImage(open(self.config["notification_email_replacement_img"], "rb").read()) self.replacement_img = MIMEImage(open(replacement_img_path, "rb").read())
except IOError as e: except IOError as e:
raise RuntimeError("error reading replacement image: {}".format(e)) raise RuntimeError("error reading replacement image: {}".format(e))
else: else:
self.replacement_img.add_header("Content-ID", "<removed_for_security_reasons>") self.replacement_img.add_header("Content-ID", "<removed_for_security_reasons>")
else:
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): def get_text(self, queueid, part):
"Get the mail text in html form from email 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)) soup = self.get_html_text_part(queueid, email.message_from_binary_file(fp))
# replace picture sources # replace picture sources
picture_replaced = False image_replaced = False
if self.replacement_img:
for element in soup("img"): for element in soup("img"):
if "src" in element.attrs.keys(): if "src" in element.attrs.keys():
self.logger.debug("{}: replacing image: {}".format(queueid, element["src"])) self.logger.debug("{}: replacing image: {}".format(queueid, element["src"]))
element["src"] = "cid:removed_for_security_reasons" element["src"] = "cid:removed_for_security_reasons"
picture_replaced = True image_replaced = True
# sanitizing email text of original email # sanitizing email text of original email
sanitized_text = self.sanitize(queueid, soup) sanitized_text = self.sanitize(queueid, soup)
@@ -247,10 +267,14 @@ class EMailNotification(BaseNotification):
msg["Date"] = email.utils.formatdate() msg["Date"] = email.utils.formatdate()
msg.attach(MIMEText(htmltext, "html", 'UTF-8')) msg.attach(MIMEText(htmltext, "html", 'UTF-8'))
if picture_replaced: if image_replaced:
self.logger.debug("{}: attaching notification_replacement_img".format(queueid)) self.logger.debug("{}: attaching notification_replacement_img".format(queueid))
msg.attach(self.replacement_img) 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)) self.logger.debug("{}: sending notification email to: {}".format(queueid, recipient))
if synchronous: if synchronous:
try: try: