]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/messages.py
index: Fix alignment of front page
[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
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
523dac35
MT
107 # Create a non-multipart message
108 message = email.mime.nonmultipart.MIMENonMultipart(
109 "text", "plain", charset="utf-8",
110 )
d6df53bf 111
523dac35
MT
112 # Load template
113 t = self.template_loader.load("%s.txt" % template_name)
a82f56ea 114
523dac35
MT
115 # Render the message
116 try:
117 message_part = t.generate(**namespace)
d6df53bf 118
523dac35
MT
119 # Reset the rendered template when it could not be rendered
120 except:
121 self.template_loader.reset()
122 raise
d6df53bf 123
523dac35
MT
124 # Parse the message and extract the header
125 message_part = email.message_from_string(message_part.decode())
126 for k, v in list(message_part.items()):
127 try:
128 message.replace_header(k, v)
129 except KeyError:
130 message.add_header(k, v)
d6df53bf 131
523dac35
MT
132 # Do not encode emails in base64
133 charset = email.charset.Charset("utf-8")
134 charset.body_encoding = email.charset.QP
d6df53bf 135
523dac35
MT
136 # Set payload
137 message.set_payload(message_part.get_payload(), charset=charset)
d6df53bf
MT
138
139 # Send the message
140 self.send(recipients, message, priority=priority, headers=headers)
141
142 # In debug mode, re-compile the templates with every request
143 if self.backend.debug:
144 self.template_loader.reset()
145
146
147class Queue(misc.Object):
148 @property
149 def messages(self):
150 return self.db.query("SELECT * FROM messages \
151 WHERE time_sent IS NULL \
152 ORDER BY priority DESC, time_created ASC")
153
9fdf4fb7 154 async def send_all(self):
d6df53bf
MT
155 # Sends all messages
156 for message in self.messages:
157 self._sendmail(message)
158
159 logging.debug("All messages sent")
160
161 def _sendmail(self, message):
162 """
163 Delivers the given message to sendmail.
164 """
165 try:
166 # Parse the message from what is in the database
167 msg = email.message_from_string(message.message)
168
860cb5f3 169 logging.debug("Sending a message %s to: %s" % (
d6df53bf
MT
170 msg.get("Subject"), ", ".join(message.envelope_recipients)
171 ))
172
173 # Make sendmail command line
174 cmd = [
175 "/usr/sbin/sendmail",
176
177 # Don't treat a single line with . as end of input
178 "-oi",
179
180 # Envelope Sender
8dd434ff 181 "-f", msg.get("From") or "no-reply@ipfire.org",
d6df53bf
MT
182 ]
183
184 # Envelope Recipients
185 cmd += message.envelope_recipients
186
187 # Run sendmail and pipe the email in
188 p = subprocess.Popen(cmd, bufsize=0, close_fds=True,
189 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
190
191 stdout, stderr = p.communicate(message.message.encode("utf-8"))
192
193 # Wait until sendmail has finished
194 p.wait()
195
196 if p.returncode:
197 self.db.execute("UPDATE messages SET error_message = %s \
198 WHERE id = %s", stdout, message.id)
199
200 logging.error("Could not send mail: %s" % stdout)
201
202 # Raise all exceptions
203 except:
204 raise
205
206 else:
207 # After the email has been successfully sent, we mark it as such
208 self.db.execute("UPDATE messages SET time_sent = NOW() \
209 WHERE id = %s", message.id)
210
d6df53bf
MT
211 def cleanup(self):
212 logging.debug("Cleaning up message queue")
213
214 self.db.execute("DELETE FROM messages \
ae9d1d99 215 WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")