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