]>
Commit | Line | Data |
---|---|---|
d6df53bf MT |
1 | #!/usr/bin/python3 |
2 | ||
3 | import email | |
4 | import email.mime.multipart | |
5 | import email.utils | |
6 | import logging | |
7 | import subprocess | |
8 | import textwrap | |
9 | import tornado.gen | |
10 | import tornado.template | |
11 | ||
12 | from . import misc | |
13 | from .decorators import * | |
14 | ||
15 | class Messages(misc.Object): | |
16 | @lazy_property | |
17 | def queue(self): | |
18 | return Queue(self.backend) | |
19 | ||
20 | @lazy_property | |
21 | def template_loader(self): | |
22 | """ | |
23 | Creates a new template loader | |
24 | """ | |
5b8f7e48 MT |
25 | templates_dir = self.backend.config.get("global", "templates_dir") |
26 | assert templates_dir | |
d6df53bf MT |
27 | |
28 | return tornado.template.Loader(templates_dir, autoescape=None) | |
29 | ||
30 | def make_msgid(self): | |
31 | return email.utils.make_msgid("ipfire", domain="ipfire.org") | |
32 | ||
33 | @property | |
34 | def bounce_email_address(self): | |
35 | return self.settings.get("bounce_email_address") | |
36 | ||
37 | def send(self, recipients, message, priority=None, headers={}): | |
38 | # Convert message from string | |
39 | if not isinstance(message, email.message.Message): | |
40 | message = email.message_from_string(message) | |
41 | ||
42 | # Add a message ID if non exsist | |
43 | if not "Message-Id" in message and not "Message-ID" in message: | |
44 | message.add_header("Message-Id", self.make_msgid()) | |
45 | ||
46 | # Add any headers | |
47 | for k, v in headers: | |
48 | try: | |
49 | message.replace_header(k, v) | |
50 | except KeyError: | |
51 | message.add_header(k, v) | |
52 | ||
53 | # Add date if the message doesn't have one already | |
54 | if "Date" not in message: | |
55 | message.add_header("Date", email.utils.formatdate()) | |
56 | ||
57 | # Send any errors to the bounce address | |
58 | if self.bounce_email_address: | |
59 | message.add_header("Errors-To", "<%s>" % self.bounce_email_address) | |
60 | ||
61 | # Send the message | |
62 | self._send(recipients, message.as_string(), priority=priority) | |
63 | ||
64 | def send_template(self, template_name, recipients, | |
65 | sender=None, priority=None, headers={}, **kwargs): | |
66 | """ | |
67 | Send a message based on the given template | |
68 | """ | |
6347c471 MT |
69 | locale = tornado.locale.get("en_US") |
70 | ||
d6df53bf MT |
71 | # Create the required namespace to render the message |
72 | namespace = { | |
73 | # Generic Stuff | |
74 | "backend" : self.backend, | |
6347c471 MT |
75 | |
76 | # Locale | |
77 | "locale" : locale, | |
78 | "_" : locale.translate, | |
d6df53bf MT |
79 | } |
80 | namespace.update(kwargs) | |
81 | ||
82 | # Create a MIMEMultipart message. | |
83 | message = email.mime.multipart.MIMEMultipart() | |
84 | ||
85 | for extension, mime_type in (("txt", "plain"), ("html", "html")): | |
86 | try: | |
87 | t = self.template_loader.load("%s.%s" % (template_name, extension)) | |
88 | except IOError: | |
89 | continue | |
90 | ||
91 | # Render the message | |
92 | try: | |
93 | message_part = t.generate(**namespace) | |
94 | ||
95 | # Reset the rendered template when it could not be rendered | |
96 | except: | |
97 | self.template_loader.reset() | |
98 | raise | |
99 | ||
100 | # Parse the message and extract the header | |
101 | message_part = email.message_from_string(message_part.decode()) | |
102 | for k, v in list(message_part.items()): | |
103 | try: | |
104 | message.replace_header(k, v) | |
105 | except KeyError: | |
106 | message.add_header(k, v) | |
107 | ||
108 | message_body = message_part.get_payload() | |
109 | ||
110 | # Wrap texts to 120 characters per line | |
111 | if mime_type == "plain": | |
112 | message_body = wrap(message_body, 120) | |
113 | ||
114 | # Create a MIMEText object out of it | |
115 | message_part = email.mime.text.MIMEText(message_body, mime_type, "utf-8") | |
116 | ||
117 | # Attach the parts to the mime container. | |
118 | # According to RFC2046, the last part of a multipart message | |
119 | # is preferred. | |
120 | alternative.attach(message_part) | |
121 | ||
122 | # Add alternative section to outer message | |
123 | message.attach(alternative) | |
124 | ||
125 | # Send the message | |
126 | self.send(recipients, message, priority=priority, headers=headers) | |
127 | ||
128 | # In debug mode, re-compile the templates with every request | |
129 | if self.backend.debug: | |
130 | self.template_loader.reset() | |
131 | ||
132 | ||
133 | class Queue(misc.Object): | |
134 | @property | |
135 | def messages(self): | |
136 | return self.db.query("SELECT * FROM messages \ | |
137 | WHERE time_sent IS NULL \ | |
138 | ORDER BY priority DESC, time_created ASC") | |
139 | ||
140 | @tornado.gen.coroutine | |
141 | def send_all(self): | |
142 | # Sends all messages | |
143 | for message in self.messages: | |
144 | self._sendmail(message) | |
145 | ||
146 | logging.debug("All messages sent") | |
147 | ||
148 | def _sendmail(self, message): | |
149 | """ | |
150 | Delivers the given message to sendmail. | |
151 | """ | |
152 | try: | |
153 | # Parse the message from what is in the database | |
154 | msg = email.message_from_string(message.message) | |
155 | ||
156 | logging.info("Sending a message %s to: %s" % ( | |
157 | msg.get("Subject"), ", ".join(message.envelope_recipients) | |
158 | )) | |
159 | ||
160 | # Make sendmail command line | |
161 | cmd = [ | |
162 | "/usr/sbin/sendmail", | |
163 | ||
164 | # Don't treat a single line with . as end of input | |
165 | "-oi", | |
166 | ||
167 | # Envelope Sender | |
168 | "-f", msg.get("From"), | |
169 | ] | |
170 | ||
171 | # Envelope Recipients | |
172 | cmd += message.envelope_recipients | |
173 | ||
174 | # Run sendmail and pipe the email in | |
175 | p = subprocess.Popen(cmd, bufsize=0, close_fds=True, | |
176 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
177 | ||
178 | stdout, stderr = p.communicate(message.message.encode("utf-8")) | |
179 | ||
180 | # Wait until sendmail has finished | |
181 | p.wait() | |
182 | ||
183 | if p.returncode: | |
184 | self.db.execute("UPDATE messages SET error_message = %s \ | |
185 | WHERE id = %s", stdout, message.id) | |
186 | ||
187 | logging.error("Could not send mail: %s" % stdout) | |
188 | ||
189 | # Raise all exceptions | |
190 | except: | |
191 | raise | |
192 | ||
193 | else: | |
194 | # After the email has been successfully sent, we mark it as such | |
195 | self.db.execute("UPDATE messages SET time_sent = NOW() \ | |
196 | WHERE id = %s", message.id) | |
197 | ||
198 | @tornado.gen.coroutine | |
199 | def cleanup(self): | |
200 | logging.debug("Cleaning up message queue") | |
201 | ||
202 | self.db.execute("DELETE FROM messages \ | |
203 | WHERE time_sent IS NOT NULL AND time_sent >= NOW() + '1 day'::interval") | |
204 | ||
205 | ||
206 | def wrap(text, width): | |
207 | s = [] | |
208 | ||
209 | for paragraph in text.split("\n\n"): | |
210 | paragraph = textwrap.wrap(paragraph, width, | |
211 | break_long_words=False, replace_whitespace=False) | |
212 | ||
213 | if paragraph: | |
214 | s.append("\n".join(paragraph)) | |
215 | ||
216 | return "\n\n".join(s) |