]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/messages.py
Support sending HTML 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
8import subprocess
d73bba54 9import tornado.locale
d6df53bf
MT
10import tornado.template
11
a82f56ea 12from . import accounts
d6df53bf
MT
13from . import misc
14from .decorators import *
15
16class 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 """
5b8f7e48
MT
26 templates_dir = self.backend.config.get("global", "templates_dir")
27 assert templates_dir
d6df53bf
MT
28
29 return tornado.template.Loader(templates_dir, autoescape=None)
30
a82f56ea
MT
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
d6df53bf
MT
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
a82f56ea
MT
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
13204395 58 def send(self, recipients, message, priority=0, headers={}):
d6df53bf
MT
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
1de255ad
MT
74 # Read recipients from To: header
75 if not recipients:
76 recipients = message.get("To").split(", ")
77
d6df53bf
MT
78 # Add date if the message doesn't have one already
79 if "Date" not in message:
80 message.add_header("Date", email.utils.formatdate())
81
82 # Send any errors to the bounce address
83 if self.bounce_email_address:
84 message.add_header("Errors-To", "<%s>" % self.bounce_email_address)
85
86 # Send the message
87 self._send(recipients, message.as_string(), priority=priority)
88
1de255ad 89 def send_template(self, template_name, recipients=[],
13204395 90 sender=None, priority=0, headers={}, **kwargs):
d6df53bf
MT
91 """
92 Send a message based on the given template
93 """
6347c471
MT
94 locale = tornado.locale.get("en_US")
95
d6df53bf
MT
96 # Create the required namespace to render the message
97 namespace = {
98 # Generic Stuff
99 "backend" : self.backend,
6347c471
MT
100
101 # Locale
102 "locale" : locale,
103 "_" : locale.translate,
d6df53bf
MT
104 }
105 namespace.update(kwargs)
106
05873be1
MT
107 # Create an alternating multipart message to show HTML or text
108 message = email.mime.multipart.MIMEMultipart("alternative")
d6df53bf 109
05873be1
MT
110 for extension, mimetype in (("txt", "plain"), ("html", "html")):
111 try:
112 t = self.template_loader.load("%s.%s" % (template_name, extension))
113 except IOError as e:
114 # Ignore if the HTML template does not exist
115 if extension == "html":
116 continue
d6df53bf 117
05873be1
MT
118 # Raise all other exceptions
119 raise e
d6df53bf 120
05873be1 121 # Render the message
523dac35 122 try:
05873be1 123 message_part = t.generate(**namespace)
d6df53bf 124
05873be1
MT
125 # Reset the rendered template when it could not be rendered
126 except:
127 self.template_loader.reset()
128 raise
d6df53bf 129
05873be1
MT
130 # Parse the message and extract the header
131 message_part = email.message_from_string(message_part.decode())
132
133 for header in message_part:
134 try:
135 message.replace_header(header, message_part[header])
136 except KeyError:
137 message.add_header(header, message_part[header])
138
139 # Create a MIMEText object out of it
140 message_part = email.mime.text.MIMEText(
141 message_part.get_payload(), mimetype)
142
143 # Attach the parts to the mime container.
144 # According to RFC2046, the last part of a multipart message
145 # is preferred.
146 message.attach(message_part)
d6df53bf
MT
147
148 # Send the message
149 self.send(recipients, message, priority=priority, headers=headers)
150
151 # In debug mode, re-compile the templates with every request
152 if self.backend.debug:
153 self.template_loader.reset()
154
05873be1
MT
155 async def send_cli(self, template, recipient):
156 """
157 Send a test message from the CLI
158 """
159 account = self.backend.accounts.get_by_mail(recipient)
160
161 return self.send_template(template, recipients=[recipient,],
162 account=account)
163
d6df53bf
MT
164
165class Queue(misc.Object):
166 @property
167 def messages(self):
168 return self.db.query("SELECT * FROM messages \
169 WHERE time_sent IS NULL \
170 ORDER BY priority DESC, time_created ASC")
171
9fdf4fb7 172 async def send_all(self):
d6df53bf
MT
173 # Sends all messages
174 for message in self.messages:
175 self._sendmail(message)
176
177 logging.debug("All messages sent")
178
179 def _sendmail(self, message):
180 """
181 Delivers the given message to sendmail.
182 """
183 try:
184 # Parse the message from what is in the database
185 msg = email.message_from_string(message.message)
186
860cb5f3 187 logging.debug("Sending a message %s to: %s" % (
d6df53bf
MT
188 msg.get("Subject"), ", ".join(message.envelope_recipients)
189 ))
190
191 # Make sendmail command line
192 cmd = [
193 "/usr/sbin/sendmail",
194
195 # Don't treat a single line with . as end of input
196 "-oi",
197
198 # Envelope Sender
8dd434ff 199 "-f", msg.get("From") or "no-reply@ipfire.org",
d6df53bf
MT
200 ]
201
202 # Envelope Recipients
203 cmd += message.envelope_recipients
204
205 # Run sendmail and pipe the email in
206 p = subprocess.Popen(cmd, bufsize=0, close_fds=True,
207 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
208
209 stdout, stderr = p.communicate(message.message.encode("utf-8"))
210
211 # Wait until sendmail has finished
212 p.wait()
213
214 if p.returncode:
215 self.db.execute("UPDATE messages SET error_message = %s \
216 WHERE id = %s", stdout, message.id)
217
218 logging.error("Could not send mail: %s" % stdout)
219
220 # Raise all exceptions
221 except:
222 raise
223
224 else:
225 # After the email has been successfully sent, we mark it as such
226 self.db.execute("UPDATE messages SET time_sent = NOW() \
227 WHERE id = %s", message.id)
228
d6df53bf
MT
229 def cleanup(self):
230 logging.debug("Cleaning up message queue")
231
232 self.db.execute("DELETE FROM messages \
ae9d1d99 233 WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")