]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/messages.py
28b717c07f2be70f9b40ee6196a7ee3906071061
[ipfire.org.git] / src / backend / messages.py
1 #!/usr/bin/python3
2
3 import email
4 import email.charset
5 import email.mime.nonmultipart
6 import email.utils
7 import logging
8 import subprocess
9 import tornado.locale
10 import tornado.template
11
12 from . import accounts
13 from . import misc
14 from .decorators import *
15
16 class 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 """
26 templates_dir = self.backend.config.get("global", "templates_dir")
27 assert templates_dir
28
29 return tornado.template.Loader(templates_dir, autoescape=None)
30
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
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
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
58 def send(self, recipients, message, priority=0, headers={}):
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 # Read recipients from To: header
75 if not recipients:
76 recipients = message.get("To").split(", ")
77
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
89 def send_template(self, template_name, recipients=[],
90 sender=None, priority=0, headers={}, **kwargs):
91 """
92 Send a message based on the given template
93 """
94 locale = tornado.locale.get("en_US")
95
96 # Create the required namespace to render the message
97 namespace = {
98 # Generic Stuff
99 "backend" : self.backend,
100
101 # Locale
102 "locale" : locale,
103 "_" : locale.translate,
104 }
105 namespace.update(kwargs)
106
107 # Create a non-multipart message
108 message = email.mime.nonmultipart.MIMENonMultipart(
109 "text", "plain", charset="utf-8",
110 )
111
112 # Load template
113 t = self.template_loader.load("%s.txt" % template_name)
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 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)
131
132 # Do not encode emails in base64
133 charset = email.charset.Charset("utf-8")
134 charset.body_encoding = email.charset.QP
135
136 # Set payload
137 message.set_payload(message_part.get_payload(), charset=charset)
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
147 class 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
154 async def send_all(self):
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
169 logging.debug("Sending a message %s to: %s" % (
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
181 "-f", msg.get("From") or "no-reply@ipfire.org",
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
211 def cleanup(self):
212 logging.debug("Cleaning up message queue")
213
214 self.db.execute("DELETE FROM messages \
215 WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")