]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/messages.py
817a6f10d4eccbe6a77940c47948526f19d5b51d
4 import email
.mime
.multipart
13 import tornado
.template
15 from . import accounts
18 from .decorators
import *
20 class Messages(misc
.Object
):
23 return Queue(self
.backend
)
26 def template_loader(self
):
28 Creates a new template loader
30 templates_dir
= self
.backend
.config
.get("global", "templates_dir")
33 return tornado
.template
.Loader(templates_dir
, autoescape
=None)
35 def make_recipient(self
, recipient
):
36 # Use the contact instead of the account
37 if isinstance(recipient
, accounts
.Account
):
38 recipient
= recipient
.email_to
40 # Fall back to pass on strings
44 return email
.utils
.make_msgid("ipfire", domain
="ipfire.org")
47 def bounce_email_address(self
):
48 return self
.settings
.get("bounce_email_address")
50 def _send(self
, message
, sender
=None, priority
=0):
51 res
= self
.db
.get("INSERT INTO messages(message, priority) \
52 VALUES(%s, %s) RETURNING id", message
, priority
)
54 logging
.debug("Message queued with ID %s" % res
.id)
56 def send(self
, message
, priority
=0, headers
={}):
57 # Convert message from string
58 if not isinstance(message
, email
.message
.Message
):
59 message
= email
.message_from_string(message
)
61 # Add a message ID if non exsist
62 if not "Message-Id" in message
and not "Message-ID" in message
:
63 message
.add_header("Message-Id", self
.make_msgid())
68 message
.replace_header(k
, v
)
70 message
.add_header(k
, v
)
72 # Add date if the message doesn't have one already
73 if "Date" not in message
:
74 message
.add_header("Date", email
.utils
.formatdate())
76 # Send any errors to the bounce address
77 if self
.bounce_email_address
:
78 message
.add_header("Errors-To", "<%s>" % self
.bounce_email_address
)
81 self
._send
(message
.as_string(), priority
=priority
)
83 def send_template(self
, template_name
,
84 sender
=None, priority
=0, headers
={}, **kwargs
):
86 Send a message based on the given template
88 locale
= tornado
.locale
.get("en_US")
90 # Create the required namespace to render the message
93 "backend" : self
.backend
,
97 "_" : locale
.translate
,
99 namespace
.update(kwargs
)
101 # Create an alternating multipart message to show HTML or text
102 message
= email
.mime
.multipart
.MIMEMultipart("alternative")
104 for extension
, mimetype
in (("txt", "plain"), ("html", "html")):
106 t
= self
.template_loader
.load("%s.%s" % (template_name
, extension
))
108 # Ignore if the HTML template does not exist
109 if extension
== "html":
112 # Raise all other exceptions
117 message_part
= t
.generate(**namespace
)
119 # Reset the rendered template when it could not be rendered
121 self
.template_loader
.reset()
124 # Parse the message and extract the header
125 message_part
= email
.message_from_string(message_part
.decode())
127 for header
in message_part
:
128 value
= message_part
[header
]
130 # Make sure addresses are properly encoded
131 realname
, address
= email
.utils
.parseaddr(value
)
132 if realname
and address
:
133 value
= email
.utils
.formataddr((realname
, address
))
136 message
.replace_header(header
, value
)
138 message
.add_header(header
, value
)
140 # Create a MIMEText object out of it
141 message_part
= email
.mime
.text
.MIMEText(
142 message_part
.get_payload(), mimetype
)
144 # Attach the parts to the mime container.
145 # According to RFC2046, the last part of a multipart message
147 message
.attach(message_part
)
150 self
.send(message
, priority
=priority
, headers
=headers
)
152 # In debug mode, re-compile the templates with every request
153 if self
.backend
.debug
:
154 self
.template_loader
.reset()
156 async def send_cli(self
, template
, recipient
):
158 Send a test message from the CLI
160 account
= self
.backend
.accounts
.get_by_mail(recipient
)
162 posts
= list(self
.backend
.blog
.get_newest(limit
=5))
166 "first_name" : account
.first_name
,
167 "last_name" : account
.last_name
,
169 "email" : account
.email
,
171 # Random activation/reset codes
172 "activation_code" : util
.random_string(36),
173 "reset_code" : util
.random_string(64),
175 # The latest blog post
176 "post" : random
.choice(posts
),
179 return self
.send_template(template
, **kwargs
)
182 class Queue(misc
.Object
):
185 return self
.db
.query("SELECT * FROM messages \
186 WHERE time_sent IS NULL \
187 ORDER BY priority DESC, time_created ASC")
192 Connection to the local mail relay
194 hostname
= socket
.getfqdn()
196 # Open SMTP connection
197 conn
= smtplib
.SMTP(hostname
)
199 # Start TLS connection
200 conn
.starttls(context
=self
.backend
.ssl_context
)
204 async def send_all(self
):
206 for message
in self
.messages
:
209 logging
.debug("All messages sent")
211 def send(self
, message
):
213 Delivers the given message the local mail relay
215 # Parse the message from what is in the database
216 msg
= email
.message_from_string(message
.message
)
218 logging
.debug("Sending a message %s to: %s" % (
219 msg
.get("Subject"), msg
.get("To"),
223 rejected_recipients
= {}
225 # Try delivering the email
227 rejected_recipients
= self
.relay
.send_message(msg
)
229 except smtplib
.SMTPRecipientsRefused
as e
:
230 rejected_recipients
= e
.recipients
232 except smtplib
.SMTPException
as e
:
233 logging
.error("SMTP Exception: %s" % e
)
234 error_messages
.append("%s" % e
)
236 # Log all emails that could not be delivered
237 for recipient
in rejected_recipients
:
238 code
, reason
= rejected_recipients
[recipient
]
240 error_messages
.append("Recipient refused: %s - %s (%s)" % \
241 (recipient
, code
, reason
.decode()))
244 self
.db
.execute("UPDATE messages SET error_message = %s \
245 WHERE id = %s", "; ".join(error_messages
), message
.id)
247 logging
.error("Could not send email: %s" % message
.id)
248 for line
in error_messages
:
251 # After the email has been successfully sent, we mark it as such
252 self
.db
.execute("UPDATE messages SET time_sent = NOW() \
253 WHERE id = %s", message
.id)
256 logging
.debug("Cleaning up message queue")
258 self
.db
.execute("DELETE FROM messages \
259 WHERE time_sent IS NOT NULL AND time_sent <= NOW() - '30 day'::interval")