]>
Commit | Line | Data |
---|---|---|
d6df53bf MT |
1 | #!/usr/bin/python3 |
2 | ||
3 | import email | |
523dac35 MT |
4 | import email.charset |
5 | import email.mime.nonmultipart | |
d6df53bf MT |
6 | import email.utils |
7 | import logging | |
8 | import subprocess | |
d73bba54 | 9 | import tornado.locale |
d6df53bf MT |
10 | import tornado.template |
11 | ||
a82f56ea | 12 | from . import accounts |
d6df53bf MT |
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 | """ | |
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 | ||
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 | ||
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") |