#!/usr/bin/env python
# cardinal_pythonlib/django/mail.py
"""
===============================================================================
Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).
This file is part of cardinal_pythonlib.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
**E-mail backend for Django that fixes a TLS bug.**
"""
import smtplib
import ssl
# noinspection PyUnresolvedReferences
from django.core.mail.backends.smtp import EmailBackend
# noinspection PyUnresolvedReferences
from django.core.mail.utils import DNS_NAME
from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
log = get_brace_style_log_with_null_handler(__name__)
[docs]class SmtpEmailBackendTls1(EmailBackend):
"""
Overrides ``django.core.mail.backends.smtp.EmailBackend`` to require TLS
v1.
Use this if your existing TLS server gives the error:
.. code-block:: none
ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:600)
... which appears to be a manifestation of changes in Python's
``smtplib`` library, which relies on its ``ssl`` library, which relies on
OpenSSL. Something here has changed and now some servers that only support
TLS version 1.0 don't work. In these situations, the following code fails:
.. code-block:: python
import smtplib
s = smtplib.SMTP(host, port) # port typically 587
print(s.help()) # so we know we're communicating
s.ehlo() # ditto
s.starttls() # fails with ssl.SSLEOFError as above
and this works:
.. code-block:: python
import smtplib
import ssl
s = smtplib.SMTP(host, port)
c = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
s.ehlo()
s.starttls(context=c) # works
then to send a simple message:
.. code-block:: python
s.login(user, password)
s.sendmail(sender, recipient, message)
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if not self.use_tls:
raise ValueError("This backend is specifically for TLS.")
# self.use_ssl will be False, by the superclass's checks
@staticmethod
def _protocol():
# noinspection PyUnresolvedReferences
return ssl.PROTOCOL_TLSv1
[docs] def open(self) -> bool:
"""
Ensures we have a connection to the email server. Returns whether or
not a new connection was required (True or False).
"""
if self.connection:
# Nothing to do if the connection is already open.
return False
connection_params = {"local_hostname": DNS_NAME.get_fqdn()}
if self.timeout is not None:
connection_params["timeout"] = self.timeout
try:
self.connection = smtplib.SMTP(
self.host, self.port, **connection_params
)
# TLS
context = ssl.SSLContext(self._protocol())
if self.ssl_certfile:
context.load_cert_chain(
certfile=self.ssl_certfile, keyfile=self.ssl_keyfile
)
self.connection.ehlo()
self.connection.starttls(context=context)
self.connection.ehlo()
if self.username and self.password:
self.connection.login(self.username, self.password)
log.debug("Successful SMTP connection/login")
else:
log.debug("Successful SMTP connection (without login)")
return True
except smtplib.SMTPException:
log.debug("SMTP connection and/or login failed")
if not self.fail_silently:
raise