]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/messages.py
messages: Import code that handles emails
[ipfire.org.git] / src / backend / messages.py
1 #!/usr/bin/python3
2
3 import email
4 import email.mime.multipart
5 import email.utils
6 import logging
7 import subprocess
8 import textwrap
9 import tornado.gen
10 import tornado.template
11
12 from . import misc
13 from .decorators import *
14
15 class Messages(misc.Object):
16 @lazy_property
17 def queue(self):
18 return Queue(self.backend)
19
20 @lazy_property
21 def template_loader(self):
22 """
23 Creates a new template loader
24 """
25 templates_dir = os.path.join(self.settings.get("templates_dir"), "messages")
26
27 return tornado.template.Loader(templates_dir, autoescape=None)
28
29 def make_msgid(self):
30 return email.utils.make_msgid("ipfire", domain="ipfire.org")
31
32 @property
33 def bounce_email_address(self):
34 return self.settings.get("bounce_email_address")
35
36 def send(self, recipients, message, priority=None, headers={}):
37 # Convert message from string
38 if not isinstance(message, email.message.Message):
39 message = email.message_from_string(message)
40
41 # Add a message ID if non exsist
42 if not "Message-Id" in message and not "Message-ID" in message:
43 message.add_header("Message-Id", self.make_msgid())
44
45 # Add any headers
46 for k, v in headers:
47 try:
48 message.replace_header(k, v)
49 except KeyError:
50 message.add_header(k, v)
51
52 # Add date if the message doesn't have one already
53 if "Date" not in message:
54 message.add_header("Date", email.utils.formatdate())
55
56 # Send any errors to the bounce address
57 if self.bounce_email_address:
58 message.add_header("Errors-To", "<%s>" % self.bounce_email_address)
59
60 # Send the message
61 self._send(recipients, message.as_string(), priority=priority)
62
63 def send_template(self, template_name, recipients,
64 sender=None, priority=None, headers={}, **kwargs):
65 """
66 Send a message based on the given template
67 """
68 # Create the required namespace to render the message
69 namespace = {
70 # Generic Stuff
71 "backend" : self.backend,
72 }
73 namespace.update(kwargs)
74
75 # Create a MIMEMultipart message.
76 message = email.mime.multipart.MIMEMultipart()
77
78 for extension, mime_type in (("txt", "plain"), ("html", "html")):
79 try:
80 t = self.template_loader.load("%s.%s" % (template_name, extension))
81 except IOError:
82 continue
83
84 # Render the message
85 try:
86 message_part = t.generate(**namespace)
87
88 # Reset the rendered template when it could not be rendered
89 except:
90 self.template_loader.reset()
91 raise
92
93 # Parse the message and extract the header
94 message_part = email.message_from_string(message_part.decode())
95 for k, v in list(message_part.items()):
96 try:
97 message.replace_header(k, v)
98 except KeyError:
99 message.add_header(k, v)
100
101 message_body = message_part.get_payload()
102
103 # Wrap texts to 120 characters per line
104 if mime_type == "plain":
105 message_body = wrap(message_body, 120)
106
107 # Create a MIMEText object out of it
108 message_part = email.mime.text.MIMEText(message_body, mime_type, "utf-8")
109
110 # Attach the parts to the mime container.
111 # According to RFC2046, the last part of a multipart message
112 # is preferred.
113 alternative.attach(message_part)
114
115 # Add alternative section to outer message
116 message.attach(alternative)
117
118 # Send the message
119 self.send(recipients, message, priority=priority, headers=headers)
120
121 # In debug mode, re-compile the templates with every request
122 if self.backend.debug:
123 self.template_loader.reset()
124
125
126 class Queue(misc.Object):
127 @property
128 def messages(self):
129 return self.db.query("SELECT * FROM messages \
130 WHERE time_sent IS NULL \
131 ORDER BY priority DESC, time_created ASC")
132
133 @tornado.gen.coroutine
134 def send_all(self):
135 # Sends all messages
136 for message in self.messages:
137 self._sendmail(message)
138
139 logging.debug("All messages sent")
140
141 def _sendmail(self, message):
142 """
143 Delivers the given message to sendmail.
144 """
145 try:
146 # Parse the message from what is in the database
147 msg = email.message_from_string(message.message)
148
149 logging.info("Sending a message %s to: %s" % (
150 msg.get("Subject"), ", ".join(message.envelope_recipients)
151 ))
152
153 # Make sendmail command line
154 cmd = [
155 "/usr/sbin/sendmail",
156
157 # Don't treat a single line with . as end of input
158 "-oi",
159
160 # Envelope Sender
161 "-f", msg.get("From"),
162 ]
163
164 # Envelope Recipients
165 cmd += message.envelope_recipients
166
167 # Run sendmail and pipe the email in
168 p = subprocess.Popen(cmd, bufsize=0, close_fds=True,
169 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
170
171 stdout, stderr = p.communicate(message.message.encode("utf-8"))
172
173 # Wait until sendmail has finished
174 p.wait()
175
176 if p.returncode:
177 self.db.execute("UPDATE messages SET error_message = %s \
178 WHERE id = %s", stdout, message.id)
179
180 logging.error("Could not send mail: %s" % stdout)
181
182 # Raise all exceptions
183 except:
184 raise
185
186 else:
187 # After the email has been successfully sent, we mark it as such
188 self.db.execute("UPDATE messages SET time_sent = NOW() \
189 WHERE id = %s", message.id)
190
191 @tornado.gen.coroutine
192 def cleanup(self):
193 logging.debug("Cleaning up message queue")
194
195 self.db.execute("DELETE FROM messages \
196 WHERE time_sent IS NOT NULL AND time_sent >= NOW() + '1 day'::interval")
197
198
199 def wrap(text, width):
200 s = []
201
202 for paragraph in text.split("\n\n"):
203 paragraph = textwrap.wrap(paragraph, width,
204 break_long_words=False, replace_whitespace=False)
205
206 if paragraph:
207 s.append("\n".join(paragraph))
208
209 return "\n\n".join(s)