]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/messages.py
5 import email
.mime
.multipart
17 import tornado
.template
19 from . import accounts
22 from .decorators
import *
24 # Encode emails in UTF-8 by default
25 email
.charset
.add_charset("utf-8", email
.charset
.SHORTEST
, email
.charset
.QP
, "utf-8")
27 class Messages(misc
.Object
):
30 return Queue(self
.backend
)
33 def template_loader(self
):
35 Creates a new template loader
37 templates_dir
= self
.backend
.config
.get("global", "templates_dir")
42 "embed_image" : self
.embed_image
,
45 return tornado
.template
.Loader(templates_dir
, namespace
=namespace
, autoescape
=None)
47 def make_recipient(self
, recipient
):
48 # Use the contact instead of the account
49 if isinstance(recipient
, accounts
.Account
):
50 recipient
= recipient
.email_to
52 # Fall back to pass on strings
56 return email
.utils
.make_msgid("ipfire", domain
="ipfire.org")
59 def bounce_email_address(self
):
60 return self
.settings
.get("bounce_email_address")
62 def _send(self
, message
, sender
=None, priority
=0):
63 res
= self
.db
.get("INSERT INTO messages(message, priority) \
64 VALUES(%s, %s) RETURNING id", message
, priority
)
66 logging
.debug("Message queued with ID %s" % res
.id)
68 def send(self
, message
, priority
=0, headers
={}):
69 # Convert message from string
70 if not isinstance(message
, email
.message
.Message
):
71 message
= email
.message_from_string(message
)
73 # Add a message ID if non exsist
74 if not "Message-Id" in message
and not "Message-ID" in message
:
75 message
.add_header("Message-Id", self
.make_msgid())
80 message
.replace_header(k
, v
)
82 message
.add_header(k
, v
)
84 # Add date if the message doesn't have one already
85 if "Date" not in message
:
86 message
.add_header("Date", email
.utils
.formatdate())
88 # Send any errors to the bounce address
89 if self
.bounce_email_address
:
90 message
.add_header("Errors-To", "<%s>" % self
.bounce_email_address
)
93 self
._send
(message
.as_string(), priority
=priority
)
95 def send_template(self
, template_name
,
96 sender
=None, priority
=0, headers
={}, **kwargs
):
98 Send a message based on the given template
100 locale
= tornado
.locale
.get("en_US")
102 # Create the required namespace to render the message
105 "backend" : self
.backend
,
109 "_" : locale
.translate
,
111 namespace
.update(kwargs
)
113 # Create an alternating multipart message to show HTML or text
114 message
= email
.mime
.multipart
.MIMEMultipart("alternative")
116 for extension
, mimetype
in (("txt", "plain"), ("html", "html")):
118 t
= self
.template_loader
.load("%s.%s" % (template_name
, extension
))
120 # Ignore if the HTML template does not exist
121 if extension
== "html":
124 # Raise all other exceptions
129 message_part
= t
.generate(**namespace
)
131 # Reset the rendered template when it could not be rendered
133 self
.template_loader
.reset()
136 # Parse the message and extract the header
137 message_part
= email
.message_from_string(message_part
.decode())
139 for header
in message_part
:
140 value
= message_part
[header
]
142 # Make sure addresses are properly encoded
143 realname
, address
= email
.utils
.parseaddr(value
)
144 if realname
and address
:
145 value
= email
.utils
.formataddr((realname
, address
))
148 message
.replace_header(header
, value
)
150 message
.add_header(header
, value
)
153 if extension
== "html":
154 message_part
= self
._inline
_css
(message_part
)
156 # Create a MIMEText object out of it
157 message_part
= email
.mime
.text
.MIMEText(
158 message_part
.get_payload(), mimetype
)
160 # Attach the parts to the mime container.
161 # According to RFC2046, the last part of a multipart message
163 message
.attach(message_part
)
166 self
.send(message
, priority
=priority
, headers
=headers
)
168 # In debug mode, re-compile the templates with every request
169 if self
.backend
.debug
:
170 self
.template_loader
.reset()
172 def _inline_css(self
, part
):
174 Inlines any CSS into style attributes
177 payload
= part
.get_payload()
180 p
= pynliner
.Pynliner().from_string(payload
)
185 # Set the payload again
186 part
.set_payload(payload
)
190 def embed_image(self
, path
):
191 static_dir
= self
.backend
.config
.get("global", "static_dir")
194 # Make the path absolute
195 path
= os
.path
.join(static_dir
, path
)
198 mimetype
, encoding
= mimetypes
.guess_type(path
)
201 with
open(path
, "rb") as f
:
204 # Convert data into base64
205 data
= base64
.b64encode(data
)
208 return "data:%s;base64,%s" % (mimetype
, data
.decode())
210 async def send_cli(self
, template
, recipient
):
212 Send a test message from the CLI
214 account
= self
.backend
.accounts
.get_by_mail(recipient
)
216 posts
= list(self
.backend
.blog
.get_newest(limit
=5))
220 "first_name" : account
.first_name
,
221 "last_name" : account
.last_name
,
223 "email" : account
.email
,
225 # Random activation/reset codes
226 "activation_code" : util
.random_string(36),
227 "reset_code" : util
.random_string(64),
229 # The latest blog post
230 "post" : random
.choice(posts
),
233 return self
.send_template(template
, **kwargs
)
236 class Queue(misc
.Object
):
239 return self
.db
.query("SELECT * FROM messages \
240 WHERE time_sent IS NULL \
241 ORDER BY priority DESC, time_created ASC")
246 Connection to the local mail relay
248 hostname
= socket
.getfqdn()
250 # Open SMTP connection
251 conn
= smtplib
.SMTP(hostname
)
253 # Start TLS connection
254 conn
.starttls(context
=self
.backend
.ssl_context
)
258 async def send_all(self
):
260 for message
in self
.messages
:
263 logging
.debug("All messages sent")
265 def send(self
, message
):
267 Delivers the given message the local mail relay
269 # Parse the message from what is in the database
270 msg
= email
.message_from_string(message
.message
)
272 logging
.debug("Sending a message %s to: %s" % (
273 msg
.get("Subject"), msg
.get("To"),
277 rejected_recipients
= {}
279 # Try delivering the email
281 rejected_recipients
= self
.relay
.send_message(msg
)
283 except smtplib
.SMTPRecipientsRefused
as e
:
284 rejected_recipients
= e
.recipients
286 except smtplib
.SMTPException
as e
:
287 logging
.error("SMTP Exception: %s" % e
)
288 error_messages
.append("%s" % e
)
290 # Log all emails that could not be delivered
291 for recipient
in rejected_recipients
:
292 code
, reason
= rejected_recipients
[recipient
]
294 error_messages
.append("Recipient refused: %s - %s (%s)" % \
295 (recipient
, code
, reason
.decode()))
298 self
.db
.execute("UPDATE messages SET error_message = %s \
299 WHERE id = %s", "; ".join(error_messages
), message
.id)
301 logging
.error("Could not send email: %s" % message
.id)
302 for line
in error_messages
:
305 # After the email has been successfully sent, we mark it as such
306 self
.db
.execute("UPDATE messages SET time_sent = NOW() \
307 WHERE id = %s", message
.id)
310 logging
.debug("Cleaning up message queue")
312 self
.db
.execute("DELETE FROM messages \
313 WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")