]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/messages.py
people: Implement password reset
[ipfire.org.git] / src / backend / messages.py
CommitLineData
d6df53bf
MT
1#!/usr/bin/python3
2
3import email
523dac35
MT
4import email.charset
5import email.mime.nonmultipart
d6df53bf
MT
6import email.utils
7import logging
8import subprocess
d6df53bf
MT
9import tornado.gen
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
74 # Add date if the message doesn't have one already
75 if "Date" not in message:
76 message.add_header("Date", email.utils.formatdate())
77
78 # Send any errors to the bounce address
79 if self.bounce_email_address:
80 message.add_header("Errors-To", "<%s>" % self.bounce_email_address)
81
82 # Send the message
83 self._send(recipients, message.as_string(), priority=priority)
84
85 def send_template(self, template_name, recipients,
13204395 86 sender=None, priority=0, headers={}, **kwargs):
d6df53bf
MT
87 """
88 Send a message based on the given template
89 """
6347c471
MT
90 locale = tornado.locale.get("en_US")
91
d6df53bf
MT
92 # Create the required namespace to render the message
93 namespace = {
94 # Generic Stuff
95 "backend" : self.backend,
6347c471
MT
96
97 # Locale
98 "locale" : locale,
99 "_" : locale.translate,
d6df53bf
MT
100 }
101 namespace.update(kwargs)
102
523dac35
MT
103 # Create a non-multipart message
104 message = email.mime.nonmultipart.MIMENonMultipart(
105 "text", "plain", charset="utf-8",
106 )
d6df53bf 107
523dac35
MT
108 # Load template
109 t = self.template_loader.load("%s.txt" % template_name)
a82f56ea 110
523dac35
MT
111 # Render the message
112 try:
113 message_part = t.generate(**namespace)
d6df53bf 114
523dac35
MT
115 # Reset the rendered template when it could not be rendered
116 except:
117 self.template_loader.reset()
118 raise
d6df53bf 119
523dac35
MT
120 # Parse the message and extract the header
121 message_part = email.message_from_string(message_part.decode())
122 for k, v in list(message_part.items()):
123 try:
124 message.replace_header(k, v)
125 except KeyError:
126 message.add_header(k, v)
d6df53bf 127
523dac35
MT
128 # Do not encode emails in base64
129 charset = email.charset.Charset("utf-8")
130 charset.body_encoding = email.charset.QP
d6df53bf 131
523dac35
MT
132 # Set payload
133 message.set_payload(message_part.get_payload(), charset=charset)
d6df53bf
MT
134
135 # Send the message
136 self.send(recipients, message, priority=priority, headers=headers)
137
138 # In debug mode, re-compile the templates with every request
139 if self.backend.debug:
140 self.template_loader.reset()
141
142
143class Queue(misc.Object):
144 @property
145 def messages(self):
146 return self.db.query("SELECT * FROM messages \
147 WHERE time_sent IS NULL \
148 ORDER BY priority DESC, time_created ASC")
149
150 @tornado.gen.coroutine
151 def send_all(self):
152 # Sends all messages
153 for message in self.messages:
154 self._sendmail(message)
155
156 logging.debug("All messages sent")
157
158 def _sendmail(self, message):
159 """
160 Delivers the given message to sendmail.
161 """
162 try:
163 # Parse the message from what is in the database
164 msg = email.message_from_string(message.message)
165
860cb5f3 166 logging.debug("Sending a message %s to: %s" % (
d6df53bf
MT
167 msg.get("Subject"), ", ".join(message.envelope_recipients)
168 ))
169
170 # Make sendmail command line
171 cmd = [
172 "/usr/sbin/sendmail",
173
174 # Don't treat a single line with . as end of input
175 "-oi",
176
177 # Envelope Sender
8dd434ff 178 "-f", msg.get("From") or "no-reply@ipfire.org",
d6df53bf
MT
179 ]
180
181 # Envelope Recipients
182 cmd += message.envelope_recipients
183
184 # Run sendmail and pipe the email in
185 p = subprocess.Popen(cmd, bufsize=0, close_fds=True,
186 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
187
188 stdout, stderr = p.communicate(message.message.encode("utf-8"))
189
190 # Wait until sendmail has finished
191 p.wait()
192
193 if p.returncode:
194 self.db.execute("UPDATE messages SET error_message = %s \
195 WHERE id = %s", stdout, message.id)
196
197 logging.error("Could not send mail: %s" % stdout)
198
199 # Raise all exceptions
200 except:
201 raise
202
203 else:
204 # After the email has been successfully sent, we mark it as such
205 self.db.execute("UPDATE messages SET time_sent = NOW() \
206 WHERE id = %s", message.id)
207
d6df53bf
MT
208 def cleanup(self):
209 logging.debug("Cleaning up message queue")
210
211 self.db.execute("DELETE FROM messages \
212 WHERE time_sent IS NOT NULL AND time_sent >= NOW() + '1 day'::interval")