--- /dev/null
+#!/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)