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