]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/messages.py
Use host certificate to send emails
[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
3258fa6b
MT
9import smtplib
10import socket
d6df53bf 11import subprocess
d73bba54 12import tornado.locale
d6df53bf
MT
13import tornado.template
14
a82f56ea 15from . import accounts
d6df53bf 16from . import misc
0218bd0d 17from . import util
d6df53bf
MT
18from .decorators import *
19
20class 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 """
5b8f7e48
MT
30 templates_dir = self.backend.config.get("global", "templates_dir")
31 assert templates_dir
d6df53bf
MT
32
33 return tornado.template.Loader(templates_dir, autoescape=None)
34
a82f56ea
MT
35 def make_recipient(self, recipient):
36 # Use the contact instead of the account
37 if isinstance(recipient, accounts.Account):
1a0eec62 38 recipient = recipient.email_to
a82f56ea
MT
39
40 # Fall back to pass on strings
41 return recipient
42
d6df53bf
MT
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
5eebd27e
MT
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)
a82f56ea
MT
53
54 logging.debug("Message queued with ID %s" % res.id)
55
5eebd27e 56 def send(self, message, priority=0, headers={}):
d6df53bf
MT
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
5eebd27e 81 self._send(message.as_string(), priority=priority)
d6df53bf 82
5eebd27e 83 def send_template(self, template_name,
13204395 84 sender=None, priority=0, headers={}, **kwargs):
d6df53bf
MT
85 """
86 Send a message based on the given template
87 """
6347c471
MT
88 locale = tornado.locale.get("en_US")
89
d6df53bf
MT
90 # Create the required namespace to render the message
91 namespace = {
92 # Generic Stuff
93 "backend" : self.backend,
6347c471
MT
94
95 # Locale
96 "locale" : locale,
97 "_" : locale.translate,
d6df53bf
MT
98 }
99 namespace.update(kwargs)
100
05873be1
MT
101 # Create an alternating multipart message to show HTML or text
102 message = email.mime.multipart.MIMEMultipart("alternative")
d6df53bf 103
05873be1
MT
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
d6df53bf 111
05873be1
MT
112 # Raise all other exceptions
113 raise e
d6df53bf 114
05873be1 115 # Render the message
523dac35 116 try:
05873be1 117 message_part = t.generate(**namespace)
d6df53bf 118
05873be1
MT
119 # Reset the rendered template when it could not be rendered
120 except:
121 self.template_loader.reset()
122 raise
d6df53bf 123
05873be1
MT
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:
2eec2c6a
MT
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
05873be1 135 try:
2eec2c6a 136 message.replace_header(header, value)
05873be1 137 except KeyError:
2eec2c6a 138 message.add_header(header, value)
05873be1
MT
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)
d6df53bf
MT
148
149 # Send the message
5eebd27e 150 self.send(message, priority=priority, headers=headers)
d6df53bf
MT
151
152 # In debug mode, re-compile the templates with every request
153 if self.backend.debug:
154 self.template_loader.reset()
155
05873be1
MT
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
aee57270
MT
162 posts = list(self.backend.blog.get_newest(limit=5))
163
0218bd0d
MT
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),
aee57270
MT
174
175 # The latest blog post
176 "post" : random.choice(posts),
0218bd0d
MT
177 }
178
5eebd27e 179 return self.send_template(template, **kwargs)
05873be1 180
d6df53bf
MT
181
182class 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
3258fa6b
MT
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
59468469 200 conn.starttls(context=self.backend.ssl_context)
3258fa6b
MT
201
202 return conn
203
9fdf4fb7 204 async def send_all(self):
d6df53bf
MT
205 # Sends all messages
206 for message in self.messages:
3258fa6b 207 self.send(message)
d6df53bf
MT
208
209 logging.debug("All messages sent")
210
3258fa6b 211 def send(self, message):
d6df53bf 212 """
3258fa6b 213 Delivers the given message the local mail relay
d6df53bf 214 """
3258fa6b
MT
215 # Parse the message from what is in the database
216 msg = email.message_from_string(message.message)
d6df53bf 217
3258fa6b 218 logging.debug("Sending a message %s to: %s" % (
5eebd27e 219 msg.get("Subject"), msg.get("To"),
3258fa6b 220 ))
d6df53bf 221
3258fa6b
MT
222 error_messages = []
223 rejected_recipients = {}
d6df53bf 224
3258fa6b
MT
225 # Try delivering the email
226 try:
5eebd27e 227 rejected_recipients = self.relay.send_message(msg)
d6df53bf 228
3258fa6b
MT
229 except smtplib.SMTPRecipientsRefused as e:
230 rejected_recipients = e.recipients
d6df53bf 231
3258fa6b
MT
232 except smtplib.SMTPException as e:
233 logging.error("SMTP Exception: %s" % e)
234 error_messages.append("%s" % e)
d6df53bf 235
3258fa6b
MT
236 # Log all emails that could not be delivered
237 for recipient in rejected_recipients:
238 code, reason = rejected_recipients[recipient]
d6df53bf 239
3258fa6b
MT
240 error_messages.append("Recipient refused: %s - %s (%s)" % \
241 (recipient, code, reason.decode()))
d6df53bf 242
3258fa6b
MT
243 if error_messages:
244 self.db.execute("UPDATE messages SET error_message = %s \
245 WHERE id = %s", "; ".join(error_messages), message.id)
d6df53bf 246
3258fa6b
MT
247 logging.error("Could not send email: %s" % message.id)
248 for line in error_messages:
249 logging.error(line)
d6df53bf 250
3258fa6b
MT
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)
d6df53bf 254
d6df53bf
MT
255 def cleanup(self):
256 logging.debug("Cleaning up message queue")
257
258 self.db.execute("DELETE FROM messages \
ae9d1d99 259 WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")