]>
Commit | Line | Data |
---|---|---|
d6df53bf MT |
1 | #!/usr/bin/python3 |
2 | ||
3 | import email | |
05873be1 MT |
4 | import email.mime.multipart |
5 | import email.mime.text | |
d6df53bf MT |
6 | import email.utils |
7 | import logging | |
aee57270 | 8 | import random |
d6df53bf | 9 | import subprocess |
d73bba54 | 10 | import tornado.locale |
d6df53bf MT |
11 | import tornado.template |
12 | ||
a82f56ea | 13 | from . import accounts |
d6df53bf | 14 | from . import misc |
0218bd0d | 15 | from . import util |
d6df53bf MT |
16 | from .decorators import * |
17 | ||
18 | class Messages(misc.Object): | |
19 | @lazy_property | |
20 | def queue(self): | |
21 | return Queue(self.backend) | |
22 | ||
23 | @lazy_property | |
24 | def template_loader(self): | |
25 | """ | |
26 | Creates a new template loader | |
27 | """ | |
5b8f7e48 MT |
28 | templates_dir = self.backend.config.get("global", "templates_dir") |
29 | assert templates_dir | |
d6df53bf MT |
30 | |
31 | return tornado.template.Loader(templates_dir, autoescape=None) | |
32 | ||
a82f56ea MT |
33 | def make_recipient(self, recipient): |
34 | # Use the contact instead of the account | |
35 | if isinstance(recipient, accounts.Account): | |
1a0eec62 | 36 | recipient = recipient.email_to |
a82f56ea MT |
37 | |
38 | # Fall back to pass on strings | |
39 | return recipient | |
40 | ||
d6df53bf MT |
41 | def make_msgid(self): |
42 | return email.utils.make_msgid("ipfire", domain="ipfire.org") | |
43 | ||
44 | @property | |
45 | def bounce_email_address(self): | |
46 | return self.settings.get("bounce_email_address") | |
47 | ||
a82f56ea MT |
48 | def _send(self, recipients, message, sender=None, priority=0): |
49 | if not recipients: | |
50 | raise ValueError("Empty list of recipients") | |
51 | ||
52 | # Format recipients | |
53 | recipients = [self.make_recipient(r) for r in recipients] | |
54 | ||
55 | res = self.db.get("INSERT INTO messages(message, priority, envelope_recipients) \ | |
56 | VALUES(%s, %s, %s) RETURNING id", message, priority, recipients) | |
57 | ||
58 | logging.debug("Message queued with ID %s" % res.id) | |
59 | ||
13204395 | 60 | def send(self, recipients, message, priority=0, headers={}): |
d6df53bf MT |
61 | # Convert message from string |
62 | if not isinstance(message, email.message.Message): | |
63 | message = email.message_from_string(message) | |
64 | ||
65 | # Add a message ID if non exsist | |
66 | if not "Message-Id" in message and not "Message-ID" in message: | |
67 | message.add_header("Message-Id", self.make_msgid()) | |
68 | ||
69 | # Add any headers | |
70 | for k, v in headers: | |
71 | try: | |
72 | message.replace_header(k, v) | |
73 | except KeyError: | |
74 | message.add_header(k, v) | |
75 | ||
1de255ad MT |
76 | # Read recipients from To: header |
77 | if not recipients: | |
78 | recipients = message.get("To").split(", ") | |
79 | ||
d6df53bf MT |
80 | # Add date if the message doesn't have one already |
81 | if "Date" not in message: | |
82 | message.add_header("Date", email.utils.formatdate()) | |
83 | ||
84 | # Send any errors to the bounce address | |
85 | if self.bounce_email_address: | |
86 | message.add_header("Errors-To", "<%s>" % self.bounce_email_address) | |
87 | ||
88 | # Send the message | |
89 | self._send(recipients, message.as_string(), priority=priority) | |
90 | ||
1de255ad | 91 | def send_template(self, template_name, recipients=[], |
13204395 | 92 | sender=None, priority=0, headers={}, **kwargs): |
d6df53bf MT |
93 | """ |
94 | Send a message based on the given template | |
95 | """ | |
6347c471 MT |
96 | locale = tornado.locale.get("en_US") |
97 | ||
d6df53bf MT |
98 | # Create the required namespace to render the message |
99 | namespace = { | |
100 | # Generic Stuff | |
101 | "backend" : self.backend, | |
6347c471 MT |
102 | |
103 | # Locale | |
104 | "locale" : locale, | |
105 | "_" : locale.translate, | |
d6df53bf MT |
106 | } |
107 | namespace.update(kwargs) | |
108 | ||
05873be1 MT |
109 | # Create an alternating multipart message to show HTML or text |
110 | message = email.mime.multipart.MIMEMultipart("alternative") | |
d6df53bf | 111 | |
05873be1 MT |
112 | for extension, mimetype in (("txt", "plain"), ("html", "html")): |
113 | try: | |
114 | t = self.template_loader.load("%s.%s" % (template_name, extension)) | |
115 | except IOError as e: | |
116 | # Ignore if the HTML template does not exist | |
117 | if extension == "html": | |
118 | continue | |
d6df53bf | 119 | |
05873be1 MT |
120 | # Raise all other exceptions |
121 | raise e | |
d6df53bf | 122 | |
05873be1 | 123 | # Render the message |
523dac35 | 124 | try: |
05873be1 | 125 | message_part = t.generate(**namespace) |
d6df53bf | 126 | |
05873be1 MT |
127 | # Reset the rendered template when it could not be rendered |
128 | except: | |
129 | self.template_loader.reset() | |
130 | raise | |
d6df53bf | 131 | |
05873be1 MT |
132 | # Parse the message and extract the header |
133 | message_part = email.message_from_string(message_part.decode()) | |
134 | ||
135 | for header in message_part: | |
136 | try: | |
137 | message.replace_header(header, message_part[header]) | |
138 | except KeyError: | |
139 | message.add_header(header, message_part[header]) | |
140 | ||
141 | # Create a MIMEText object out of it | |
142 | message_part = email.mime.text.MIMEText( | |
143 | message_part.get_payload(), mimetype) | |
144 | ||
145 | # Attach the parts to the mime container. | |
146 | # According to RFC2046, the last part of a multipart message | |
147 | # is preferred. | |
148 | message.attach(message_part) | |
d6df53bf MT |
149 | |
150 | # Send the message | |
151 | self.send(recipients, message, priority=priority, headers=headers) | |
152 | ||
153 | # In debug mode, re-compile the templates with every request | |
154 | if self.backend.debug: | |
155 | self.template_loader.reset() | |
156 | ||
05873be1 MT |
157 | async def send_cli(self, template, recipient): |
158 | """ | |
159 | Send a test message from the CLI | |
160 | """ | |
161 | account = self.backend.accounts.get_by_mail(recipient) | |
162 | ||
aee57270 MT |
163 | posts = list(self.backend.blog.get_newest(limit=5)) |
164 | ||
0218bd0d MT |
165 | kwargs = { |
166 | "account" : account, | |
167 | "first_name" : account.first_name, | |
168 | "last_name" : account.last_name, | |
169 | "uid" : account.uid, | |
170 | "email" : account.email, | |
171 | ||
172 | # Random activation/reset codes | |
173 | "activation_code" : util.random_string(36), | |
174 | "reset_code" : util.random_string(64), | |
aee57270 MT |
175 | |
176 | # The latest blog post | |
177 | "post" : random.choice(posts), | |
0218bd0d MT |
178 | } |
179 | ||
180 | return self.send_template(template, recipients=[recipient,], **kwargs) | |
05873be1 | 181 | |
d6df53bf MT |
182 | |
183 | class Queue(misc.Object): | |
184 | @property | |
185 | def messages(self): | |
186 | return self.db.query("SELECT * FROM messages \ | |
187 | WHERE time_sent IS NULL \ | |
188 | ORDER BY priority DESC, time_created ASC") | |
189 | ||
9fdf4fb7 | 190 | async def send_all(self): |
d6df53bf MT |
191 | # Sends all messages |
192 | for message in self.messages: | |
193 | self._sendmail(message) | |
194 | ||
195 | logging.debug("All messages sent") | |
196 | ||
197 | def _sendmail(self, message): | |
198 | """ | |
199 | Delivers the given message to sendmail. | |
200 | """ | |
201 | try: | |
202 | # Parse the message from what is in the database | |
203 | msg = email.message_from_string(message.message) | |
204 | ||
860cb5f3 | 205 | logging.debug("Sending a message %s to: %s" % ( |
d6df53bf MT |
206 | msg.get("Subject"), ", ".join(message.envelope_recipients) |
207 | )) | |
208 | ||
209 | # Make sendmail command line | |
210 | cmd = [ | |
211 | "/usr/sbin/sendmail", | |
212 | ||
213 | # Don't treat a single line with . as end of input | |
214 | "-oi", | |
215 | ||
216 | # Envelope Sender | |
8dd434ff | 217 | "-f", msg.get("From") or "no-reply@ipfire.org", |
d6df53bf MT |
218 | ] |
219 | ||
220 | # Envelope Recipients | |
221 | cmd += message.envelope_recipients | |
222 | ||
223 | # Run sendmail and pipe the email in | |
224 | p = subprocess.Popen(cmd, bufsize=0, close_fds=True, | |
225 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
226 | ||
227 | stdout, stderr = p.communicate(message.message.encode("utf-8")) | |
228 | ||
229 | # Wait until sendmail has finished | |
230 | p.wait() | |
231 | ||
232 | if p.returncode: | |
233 | self.db.execute("UPDATE messages SET error_message = %s \ | |
234 | WHERE id = %s", stdout, message.id) | |
235 | ||
236 | logging.error("Could not send mail: %s" % stdout) | |
237 | ||
238 | # Raise all exceptions | |
239 | except: | |
240 | raise | |
241 | ||
242 | else: | |
243 | # After the email has been successfully sent, we mark it as such | |
244 | self.db.execute("UPDATE messages SET time_sent = NOW() \ | |
245 | WHERE id = %s", message.id) | |
246 | ||
d6df53bf MT |
247 | def cleanup(self): |
248 | logging.debug("Cleaning up message queue") | |
249 | ||
250 | self.db.execute("DELETE FROM messages \ | |
ae9d1d99 | 251 | WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval") |