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