]> git.ipfire.org Git - ipfire.org.git/commitdiff
messages: Import code that handles emails
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 3 Dec 2018 23:34:37 +0000 (23:34 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 3 Dec 2018 23:34:37 +0000 (23:34 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/backend/base.py
src/backend/messages.py [new file with mode: 0644]
src/crontab/ipfire.org
src/scripts/ipfire.org.in

index b00350b7f3a99798e230762c5a86d2dfc4f1e5b3..c9d95919ce0306eae5b97bcf680310abcb68cb56 100644 (file)
@@ -59,6 +59,7 @@ backend_PYTHON = \
        src/backend/hwdata.py \
        src/backend/iuse.py \
        src/backend/memcached.py \
+       src/backend/messages.py \
        src/backend/mirrors.py \
        src/backend/misc.py \
        src/backend/netboot.py \
index 4e24b60adf26fe06a5c13d2c73c04f9eb3d08a3a..d5d5a45df0a0e0d685a0031777eefeec5c9d4233 100644 (file)
@@ -10,6 +10,7 @@ from . import geoip
 from . import fireinfo
 from . import iuse
 from . import memcached
+from . import messages
 from . import mirrors
 from . import netboot
 from . import nopaste
@@ -20,6 +21,7 @@ from . import talk
 from . import blog
 from . import wiki
 from . import zeiterfassung
+from .decorators import *
 
 DEFAULT_CONFIG = io.StringIO("""
 [global]
@@ -87,7 +89,9 @@ class Backend(object):
        @tornado.gen.coroutine
        def run_task(self, task, *args, **kwargs):
                tasks = {
+                       "cleanup-messages"  : self.messages.queue.cleanup,
                        "scan-files"        : self.releases.scan_files,
+                       "send-all-messages" : self.messages.queue.send_all,
                        "update-blog-feeds" : self.blog.update_feeds,
                }
 
@@ -103,3 +107,7 @@ class Backend(object):
                # we will end the program
                if r:
                        raise SystemExit(r)
+
+       @lazy_property
+       def messages(self):
+               return messages.Messages(self)
diff --git a/src/backend/messages.py b/src/backend/messages.py
new file mode 100644 (file)
index 0000000..e685081
--- /dev/null
@@ -0,0 +1,209 @@
+#!/usr/bin/python3
+
+import email
+import email.mime.multipart
+import email.utils
+import logging
+import subprocess
+import textwrap
+import tornado.gen
+import tornado.template
+
+from . import misc
+from .decorators import *
+
+class Messages(misc.Object):
+       @lazy_property
+       def queue(self):
+               return Queue(self.backend)
+
+       @lazy_property
+       def template_loader(self):
+               """
+                       Creates a new template loader
+               """
+               templates_dir = os.path.join(self.settings.get("templates_dir"), "messages")
+
+               return tornado.template.Loader(templates_dir, autoescape=None)
+
+       def make_msgid(self):
+               return email.utils.make_msgid("ipfire", domain="ipfire.org")
+
+       @property
+       def bounce_email_address(self):
+               return self.settings.get("bounce_email_address")
+
+       def send(self, recipients, message, priority=None, headers={}):
+               # Convert message from string
+               if not isinstance(message, email.message.Message):
+                       message = email.message_from_string(message)
+
+               # Add a message ID if non exsist
+               if not "Message-Id" in message and not "Message-ID" in message:
+                       message.add_header("Message-Id", self.make_msgid())
+
+               # Add any headers
+               for k, v in headers:
+                       try:
+                               message.replace_header(k, v)
+                       except KeyError:
+                               message.add_header(k, v)
+
+               # Add date if the message doesn't have one already
+               if "Date" not in message:
+                       message.add_header("Date", email.utils.formatdate())
+
+               # Send any errors to the bounce address
+               if self.bounce_email_address:
+                       message.add_header("Errors-To", "<%s>" % self.bounce_email_address)
+
+               # Send the message
+               self._send(recipients, message.as_string(), priority=priority)
+
+       def send_template(self, template_name, recipients,
+                       sender=None, priority=None, headers={}, **kwargs):
+               """
+                       Send a message based on the given template
+               """
+               # Create the required namespace to render the message
+               namespace = {
+                       # Generic Stuff
+                       "backend" : self.backend,
+               }
+               namespace.update(kwargs)
+
+               # Create a MIMEMultipart message.
+               message = email.mime.multipart.MIMEMultipart()
+
+               for extension, mime_type in (("txt", "plain"), ("html", "html")):
+                       try:
+                               t = self.template_loader.load("%s.%s" % (template_name, extension))
+                       except IOError:
+                               continue
+
+                       # Render the message
+                       try:
+                               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 k, v in list(message_part.items()):
+                               try:
+                                       message.replace_header(k, v)
+                               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)
+
+                       # Create a MIMEText object out of it
+                       message_part = email.mime.text.MIMEText(message_body, mime_type, "utf-8")
+
+                       # 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)
+
+               # Send the message
+               self.send(recipients, message, priority=priority, headers=headers)
+
+               # In debug mode, re-compile the templates with every request
+               if self.backend.debug:
+                       self.template_loader.reset()
+
+
+class Queue(misc.Object):
+       @property
+       def messages(self):
+               return self.db.query("SELECT * FROM messages \
+                       WHERE time_sent IS NULL \
+                               ORDER BY priority DESC, time_created ASC")
+
+       @tornado.gen.coroutine
+       def send_all(self):
+               # Sends all messages
+               for message in self.messages:
+                       self._sendmail(message)
+
+               logging.debug("All messages sent")
+
+       def _sendmail(self, message):
+               """
+                       Delivers the given message to sendmail.
+               """
+               try:
+                       # Parse the message from what is in the database
+                       msg = email.message_from_string(message.message)
+
+                       logging.info("Sending a message %s to: %s" % (
+                               msg.get("Subject"), ", ".join(message.envelope_recipients)
+                       ))
+
+                       # Make sendmail command line
+                       cmd = [
+                               "/usr/sbin/sendmail",
+
+                               # Don't treat a single line with . as end of input
+                               "-oi",
+
+                               # Envelope Sender
+                               "-f", msg.get("From"),
+                       ]
+
+                       # Envelope Recipients
+                       cmd += message.envelope_recipients
+
+                       # Run sendmail and pipe the email in
+                       p = subprocess.Popen(cmd, bufsize=0, close_fds=True,
+                               stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+                       stdout, stderr = p.communicate(message.message.encode("utf-8"))
+
+                       # Wait until sendmail has finished
+                       p.wait()
+
+                       if p.returncode:
+                               self.db.execute("UPDATE messages SET error_message = %s \
+                                       WHERE id = %s", stdout, message.id)
+
+                               logging.error("Could not send mail: %s" % stdout)
+
+               # Raise all exceptions
+               except:
+                       raise
+
+               else:
+                       # After the email has been successfully sent, we mark it as such
+                       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)
index 7fe6af86e6bbd59a3dfb5b29e2ff16a98246fa7a..36285cc0f8d56b6934a5f02f030c17726e139cb2 100644 (file)
@@ -3,3 +3,9 @@
 
 # Scan for release files once an hour
 0 * * * *   ipfire.org scan-files
+
+# Send messages
+* * * * *      ipfire.org send-all-messages
+
+# Cleanup once a an hour
+30 * * * *     ipfire.org cleanup-messages
index 7826bb47c316353c5ce08434045b054263cbcb50..25a0fb92819873c3d93a78b4b8a25c94a707e3a6 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!@PYTHON@
 
 import sys
 import tornado.gen