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