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