From 05873be1cb7e5df783a6a1f9f600494b8b50d6e7 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Wed, 20 Nov 2019 16:28:13 +0000 Subject: [PATCH] Support sending HTML emails Signed-off-by: Michael Tremer --- .gitignore | 1 + Makefile.am | 6 + src/backend/base.py | 1 + src/backend/messages.py | 72 ++++--- src/templates/messages/base.html | 61 ++++++ src/templates/messages/main.scss | 340 +++++++++++++++++++++++++++++++ 6 files changed, 454 insertions(+), 27 deletions(-) create mode 100644 src/templates/messages/base.html create mode 100644 src/templates/messages/main.scss diff --git a/.gitignore b/.gitignore index 2d0f70a6..777242a8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /src/static/favicon.ico /src/static/img/apple-touch-icon-*-precomposed.png /src/systemd/ipfire.org-webapp-*.service +/src/templates/messages/main.css /ipfire.org.conf.sample .DS_Store Makefile diff --git a/Makefile.am b/Makefile.am index 05b95d92..3250973e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -206,6 +206,12 @@ templates_location_DATA = \ templates_locationdir = $(templatesdir)/location +templates_messages_DATA = \ + src/templates/messages/base.html \ + src/templates/messages/main.css + +templates_messagesdir = $(templatesdir)/messages + templates_mirrors_DATA = \ src/templates/mirrors/index.html \ src/templates/mirrors/mirror.html diff --git a/src/backend/base.py b/src/backend/base.py index 80c50998..1578a015 100644 --- a/src/backend/base.py +++ b/src/backend/base.py @@ -102,6 +102,7 @@ class Backend(object): "launch-campaigns" : self.campaigns.launch_manually, "run-campaigns" : self.campaigns.run, "scan-files" : self.releases.scan_files, + "send-message" : self.messages.send_cli, "send-all-messages" : self.messages.queue.send_all, "test-blacklist" : self.geoip.test_blacklist, "test-ldap" : self.accounts.test_ldap, diff --git a/src/backend/messages.py b/src/backend/messages.py index 28b717c0..8063ce39 100644 --- a/src/backend/messages.py +++ b/src/backend/messages.py @@ -1,8 +1,8 @@ #!/usr/bin/python3 import email -import email.charset -import email.mime.nonmultipart +import email.mime.multipart +import email.mime.text import email.utils import logging import subprocess @@ -104,37 +104,46 @@ class Messages(misc.Object): } namespace.update(kwargs) - # Create a non-multipart message - message = email.mime.nonmultipart.MIMENonMultipart( - "text", "plain", charset="utf-8", - ) + # Create an alternating multipart message to show HTML or text + message = email.mime.multipart.MIMEMultipart("alternative") - # Load template - t = self.template_loader.load("%s.txt" % template_name) - - # Render the message - try: - message_part = t.generate(**namespace) + for extension, mimetype in (("txt", "plain"), ("html", "html")): + try: + t = self.template_loader.load("%s.%s" % (template_name, extension)) + except IOError as e: + # Ignore if the HTML template does not exist + if extension == "html": + continue - # Reset the rendered template when it could not be rendered - except: - self.template_loader.reset() - raise + # Raise all other exceptions + raise e - # Parse the message and extract the header - message_part = email.message_from_string(message_part.decode()) - for k, v in list(message_part.items()): + # Render the message try: - message.replace_header(k, v) - except KeyError: - message.add_header(k, v) + message_part = t.generate(**namespace) + + # Reset the rendered template when it could not be rendered + except: + self.template_loader.reset() + raise + + # Parse the message and extract the header + message_part = email.message_from_string(message_part.decode()) + + for header in message_part: + try: + message.replace_header(header, message_part[header]) + except KeyError: + message.add_header(header, message_part[header]) - # Do not encode emails in base64 - charset = email.charset.Charset("utf-8") - charset.body_encoding = email.charset.QP + # Create a MIMEText object out of it + message_part = email.mime.text.MIMEText( + message_part.get_payload(), mimetype) - # Set payload - message.set_payload(message_part.get_payload(), charset=charset) + # Attach the parts to the mime container. + # According to RFC2046, the last part of a multipart message + # is preferred. + message.attach(message_part) # Send the message self.send(recipients, message, priority=priority, headers=headers) @@ -143,6 +152,15 @@ class Messages(misc.Object): if self.backend.debug: self.template_loader.reset() + async def send_cli(self, template, recipient): + """ + Send a test message from the CLI + """ + account = self.backend.accounts.get_by_mail(recipient) + + return self.send_template(template, recipients=[recipient,], + account=account) + class Queue(misc.Object): @property diff --git a/src/templates/messages/base.html b/src/templates/messages/base.html new file mode 100644 index 00000000..959a3148 --- /dev/null +++ b/src/templates/messages/base.html @@ -0,0 +1,61 @@ + + + + + + + {% block title %}{% end block %} + + + + + {% block preheader %}{% end preheader %} + + + + + + + diff --git a/src/templates/messages/main.scss b/src/templates/messages/main.scss new file mode 100644 index 00000000..97c84f36 --- /dev/null +++ b/src/templates/messages/main.scss @@ -0,0 +1,340 @@ +@import "../../scss/variables"; + +@import "../../bootstrap/scss/functions"; +@import "../../bootstrap/scss/variables"; + + +// Use font sizes in px +$font-size-base: 18px; +$small-font-size: 12px; + +$h1-font-size: 48px; +$h2-font-size: 40px; +$h3-font-size: 36px; +$h4-font-size: 32px; +$headings-margin-bottom: 20px; + +$paragraph-margin-bottom: 14px; + +// Resets +img { + border: none; + -ms-interpolation-mode: bicubic; + max-width: 100%; +} + +body { + background-color: $body-bg; + font-family: $font-family-sans-serif; + -webkit-font-smoothing: antialiased; + font-size: $font-size-base; + line-height: $line-height-base; + margin: 0; + padding: 0; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} + +table { + border-collapse: separate; + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + width: 100%; + + td { + font-family: $font-family-sans-serif; + font-size: $font-size-base; + vertical-align: top; + } +} + +// Basic Styling + +.body { + background-color: $body-bg; + width: 100%; +} + +/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ +.container { + display: block; + margin: 0 auto !important; + + // Center the container + max-width: 580px; + padding: 10px; + width: 580px; +} + +/* This should also be a block element, so that it will fill 100% of the .container */ +.content { + box-sizing: border-box; + display: block; + margin: 0 auto; + max-width: 580px; + padding: 10px; +} + +// Headers, Footers, Containers + +.main { + background: $white; + color: $dark; + border-radius: $card-border-radius; + width: 100%; + + .logo { + text-align: center; + + img { + height: 196px; + padding: 24px 0 12px 0; + } + } +} + +.wrapper { + box-sizing: border-box; + padding: 20px; +} + +.content-block { + padding-bottom: 10px; + padding-top: 10px; +} + +.footer { + clear: both; + margin-top: 10px; + text-align: center; + width: 100%; + + td, p, span, a { + color: $light; + font-size: $small-font-size; + text-align: center; + } +} + +// Typography + +h1, h2, h3, h4 { + color: $dark; + font-family: $font-family-sans-serif; + font-weight: $headings-font-weight; + line-height: $headings-line-height; + margin: 0; + margin-bottom: $headings-margin-bottom; +} + +h1 { + font-size: $h1-font-size; + text-align: center; + text-transform: capitalize; +} + +p, ul, ol { + font-family: $font-family-sans-serif; + font-size: $font-size-base; + font-weight: normal; + margin: 0; + margin-bottom: $paragraph-margin-bottom; +} + +a { + color: $link-color; + text-decoration: underline; +} + +// Buttons + +.btn { + box-sizing: border-box; + width: 100%; + + > tbody > tr > td { + padding-bottom: 15px; + } + + table { + width: 100%; + + td { + background-color: #ffffff; + border-radius: $btn-border-radius; + text-align: center; + } + } + + a { + width: 100%; + background-color: #ffffff; + border: 1px solid $link-color; + border-radius: $btn-border-radius; + box-sizing: border-box; + color: $link-color; + cursor: pointer; + display: inline-block; + font-size: $font-size-base; + font-weight: $btn-font-weight; + margin: 0; + padding: $btn-padding-y $btn-padding-x; + text-decoration: none; + text-transform: uppercase; + } +} + +.btn-primary { + table td { + background-color: $link-color; + } + + a { + background-color: $link-color; + border-color: $link-color; + color: #ffffff; + } +} + +// Other + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +.align-left { + text-align: left; +} + +.clear { + clear: both; +} + +.mt-0 { + margin-top: 0; +} + +.mb-0 { + margin-bottom: 0; +} + +.preheader { + color: transparent; + display: none; + height: 0; + max-height: 0; + max-width: 0; + opacity: 0; + overflow: hidden; + mso-hide: all; + visibility: hidden; + width: 0; +} + +.powered-by a { + text-decoration: none; +} + +hr { + border: 0; + border-bottom: $hr-border-width solid $hr-border-color; + margin: 20px 0; +} + +// Make this all mobile-friendly + +@media only screen and (max-width: 620px) { + table[class=body] { + h1 { + font-size: $h1-font-size !important; + margin-bottom: $headings-margin-bottom !important; + } + + p, ul, ol, td, span, a { + font-size: $font-size-base !important; + } + + .wrapper, .article { + padding: 10px !important; + } + + .content { + padding: 0 !important; + } + + .container { + padding: 0 !important; + width: 100% !important; + } + + .main { + border-left-width: 0 !important; + border-radius: 0 !important; + border-right-width: 0 !important; + } + + .img-responsive { + height: auto !important; + max-width: 100% !important; + width: auto !important; + } + } +} + +// Hack for Dark Mode + +@media (prefers-dark-interface) { + .body { + background-color: none; + } + + .main { + background: $white !important; + color: $dark !important; + } +} + +// Hack for Outlook + +@media all { + .ExternalClass { + width: 100%; + + &, p, span, font, td, div { + line-height: 100%; + } + } + + .apple-link a { + color: inherit !important; + font-family: inherit !important; + font-size: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + text-decoration: none !important; + } + + #MessageViewBody a { + color: inherit; + text-decoration: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + } + + .btn-primary { + table td:hover { + background-color: $link-hover-color !important; + } + + a:hover { + background-color: $link-hover-color !important; + border-color: $link-hover-color !important; + } + } +} -- 2.39.2