]> git.ipfire.org Git - ipfire.org.git/blobdiff - src/backend/messages.py
Support sending HTML emails
[ipfire.org.git] / src / backend / messages.py
index c6a052973848b7bcc22d7a2611200cdf3752717a..8063ce399ede36ebe042ea8ddf49468fb29fcbaa 100644 (file)
@@ -2,13 +2,14 @@
 
 import email
 import email.mime.multipart
+import email.mime.text
 import email.utils
 import logging
 import subprocess
-import textwrap
-import tornado.gen
+import tornado.locale
 import tornado.template
 
+from . import accounts
 from . import misc
 from .decorators import *
 
@@ -27,6 +28,14 @@ class Messages(misc.Object):
 
                return tornado.template.Loader(templates_dir, autoescape=None)
 
+       def make_recipient(self, recipient):
+               # Use the contact instead of the account
+               if isinstance(recipient, accounts.Account):
+                       recipient = "%s <%s>" % (recipient, recipient.email)
+
+               # Fall back to pass on strings
+               return recipient
+
        def make_msgid(self):
                return email.utils.make_msgid("ipfire", domain="ipfire.org")
 
@@ -34,7 +43,19 @@ class Messages(misc.Object):
        def bounce_email_address(self):
                return self.settings.get("bounce_email_address")
 
-       def send(self, recipients, message, priority=None, headers={}):
+       def _send(self, recipients, message, sender=None, priority=0):
+               if not recipients:
+                       raise ValueError("Empty list of recipients")
+
+               # Format recipients
+               recipients = [self.make_recipient(r) for r in recipients]
+
+               res = self.db.get("INSERT INTO messages(message, priority, envelope_recipients) \
+                       VALUES(%s, %s, %s) RETURNING id", message, priority, recipients)
+
+               logging.debug("Message queued with ID %s" % res.id)
+
+       def send(self, recipients, message, priority=0, headers={}):
                # Convert message from string
                if not isinstance(message, email.message.Message):
                        message = email.message_from_string(message)
@@ -50,6 +71,10 @@ class Messages(misc.Object):
                        except KeyError:
                                message.add_header(k, v)
 
+               # Read recipients from To: header
+               if not recipients:
+                       recipients = message.get("To").split(", ")
+
                # Add date if the message doesn't have one already
                if "Date" not in message:
                        message.add_header("Date", email.utils.formatdate())
@@ -61,26 +86,37 @@ class Messages(misc.Object):
                # Send the message
                self._send(recipients, message.as_string(), priority=priority)
 
-       def send_template(self, template_name, recipients,
-                       sender=None, priority=None, headers={}, **kwargs):
+       def send_template(self, template_name, recipients=[],
+                       sender=None, priority=0, headers={}, **kwargs):
                """
                        Send a message based on the given template
                """
+               locale = tornado.locale.get("en_US")
+
                # Create the required namespace to render the message
                namespace = {
                        # Generic Stuff
                        "backend" : self.backend,
+
+                       # Locale
+                       "locale"  : locale,
+                       "_"       : locale.translate,
                }
                namespace.update(kwargs)
 
-               # Create a MIMEMultipart message.
-               message = email.mime.multipart.MIMEMultipart()
+               # Create an alternating multipart message to show HTML or text
+               message = email.mime.multipart.MIMEMultipart("alternative")
 
-               for extension, mime_type in (("txt", "plain"), ("html", "html")):
+               for extension, mimetype in (("txt", "plain"), ("html", "html")):
                        try:
                                t = self.template_loader.load("%s.%s" % (template_name, extension))
-                       except IOError:
-                               continue
+                       except IOError as e:
+                               # Ignore if the HTML template does not exist
+                               if extension == "html":
+                                       continue
+
+                               # Raise all other exceptions
+                               raise e
 
                        # Render the message
                        try:
@@ -93,28 +129,21 @@ class Messages(misc.Object):
 
                        # Parse the message and extract the header
                        message_part = email.message_from_string(message_part.decode())
-                       for k, v in list(message_part.items()):
+
+                       for header in message_part:
                                try:
-                                       message.replace_header(k, v)
+                                       message.replace_header(header, message_part[header])
                                except KeyError:
-                                       message.add_header(k, v)
-
-                       message_body = message_part.get_payload()
-
-                       # Wrap texts to 120 characters per line
-                       if mime_type == "plain":
-                               message_body = wrap(message_body, 120)
+                                       message.add_header(header, message_part[header])
 
                        # Create a MIMEText object out of it
-                       message_part = email.mime.text.MIMEText(message_body, mime_type, "utf-8")
+                       message_part = email.mime.text.MIMEText(
+                               message_part.get_payload(), mimetype)
 
                        # Attach the parts to the mime container.
                        # According to RFC2046, the last part of a multipart message
                        # is preferred.
-                       alternative.attach(message_part)
-
-               # Add alternative section to outer message
-               message.attach(alternative)
+                       message.attach(message_part)
 
                # Send the message
                self.send(recipients, message, priority=priority, headers=headers)
@@ -123,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
@@ -131,8 +169,7 @@ class Queue(misc.Object):
                        WHERE time_sent IS NULL \
                                ORDER BY priority DESC, time_created ASC")
 
-       @tornado.gen.coroutine
-       def send_all(self):
+       async def send_all(self):
                # Sends all messages
                for message in self.messages:
                        self._sendmail(message)
@@ -147,7 +184,7 @@ class Queue(misc.Object):
                        # Parse the message from what is in the database
                        msg = email.message_from_string(message.message)
 
-                       logging.info("Sending a message %s to: %s" % (
+                       logging.debug("Sending a message %s to: %s" % (
                                msg.get("Subject"), ", ".join(message.envelope_recipients)
                        ))
 
@@ -159,7 +196,7 @@ class Queue(misc.Object):
                                "-oi",
 
                                # Envelope Sender
-                               "-f", msg.get("From"),
+                               "-f", msg.get("From") or "no-reply@ipfire.org",
                        ]
 
                        # Envelope Recipients
@@ -189,22 +226,8 @@ class Queue(misc.Object):
                        self.db.execute("UPDATE messages SET time_sent = NOW() \
                                WHERE id = %s", message.id)
 
-       @tornado.gen.coroutine
        def cleanup(self):
                logging.debug("Cleaning up message queue")
 
                self.db.execute("DELETE FROM messages \
-                       WHERE time_sent IS NOT NULL AND time_sent >= NOW() + '1 day'::interval")
-
-
-def wrap(text, width):
-       s = []
-
-       for paragraph in text.split("\n\n"):
-               paragraph = textwrap.wrap(paragraph, width,
-                       break_long_words=False, replace_whitespace=False)
-
-               if paragraph:
-                       s.append("\n".join(paragraph))
-
-       return "\n\n".join(s)
+                       WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")