From: Michael Tremer Date: Mon, 3 Dec 2018 23:34:37 +0000 (+0000) Subject: messages: Import code that handles emails X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=d6df53bf11c1fecd5eb7659f73d642e77e58ce33;p=ipfire.org.git messages: Import code that handles emails Signed-off-by: Michael Tremer --- diff --git a/Makefile.am b/Makefile.am index b00350b7..c9d95919 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/src/backend/base.py b/src/backend/base.py index 4e24b60a..d5d5a45d 100644 --- a/src/backend/base.py +++ b/src/backend/base.py @@ -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 index 00000000..e685081d --- /dev/null +++ b/src/backend/messages.py @@ -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) diff --git a/src/crontab/ipfire.org b/src/crontab/ipfire.org index 7fe6af86..36285cc0 100644 --- a/src/crontab/ipfire.org +++ b/src/crontab/ipfire.org @@ -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 diff --git a/src/scripts/ipfire.org.in b/src/scripts/ipfire.org.in index 7826bb47..25a0fb92 100644 --- a/src/scripts/ipfire.org.in +++ b/src/scripts/ipfire.org.in @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!@PYTHON@ import sys import tornado.gen