Getting SSL error when sending email via Django
Asked Answered
G

5

6

This is what I see when I send mail via Django:

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

This is my email configuration in settings.py:

# Email Settings
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "[email protected]"
EMAIL_HOST_PASSWORD = "xxxxxxxxxxxxx"
EMAIL_USE_TLS = True

And this is how I send mail:

send_mail(
    "Subject",
    "Test message",
    settings.EMAIL_HOST_USER,
    ["[email protected]"],
    fail_silently=False,
)
Gertie answered 28/1, 2023 at 16:5 Comment(2)
What Django and Python versions are you using? We can't really help you unless you give us some version information.Mucosa
Django 4 and python 3.10Gertie
E
4

I know this topic is already some days old - but the following might be of interest for others who have to fix the same issue. But first of all thx @JavaNoScript - the answer nearly fixed it for us.

We did not want to switch off verification - but needed to get a solution for sending mails using self-signed certificates validated against our internal root CA. Therefore the Django EmailBackend had to be modified for using the internal trust store (in our case: /etc/ssl/certs/). See also python ssl context documentation.

Here the modified script from above:

import ssl
from django.core.mail.backends.smtp import EmailBackend as SMTPBackend
from django.utils.functional import cached_property
from django.conf import settings as django_settings


class EmailBackend(SMTPBackend):   
    @cached_property
    def ssl_context(self):
        if self.ssl_certfile or self.ssl_keyfile:
            ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
            # set verify location:
            if hasattr(django_settings, 'CA_PATH') and django_settings.CA_PATH is not None:
                ssl_context.load_verify_locations(capath=django_settings.CA_PATH)
            ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile)
            return ssl_context
        else:
            ssl_context = ssl.create_default_context()
            return ssl_context

The other steps are pretty similar to the ones mentioned above:

Use the custom backend rather than the standard one

# settings.py
# old value (was not included in our settings.py before):
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 
EMAIL_BACKEND = 'backend.email.EmailBackend'

And define the settings variables for sending certified mails:

# settings.py
EMAIL_SSL_CERTFILE = "/path/to/CERTFILE.crt"
EMAIL_SSL_KEYFILE = "/path/to/private/KEYFILE.key" # make sure it is accessible by the executing user
CA_PATH = '/path/to/cert/folder/' # e.g. '/etc/ssl/certs/' on ubuntu linux - this one is new 

Took us a while to figure this out.

I hope it helps some of you (although there might be more solid variable testing).

Eiland answered 13/5 at 17:58 Comment(4)
You might want to remove the else: lines that disable hostname checking and cert verification. With your settings, that code is unused, but it reduces security when certfile and keyfile are not set. (Or you could just return super().ssl_context in the else clause.)Neglectful
@medmunds: Thx for your comment - this is true. For discussion: Better set the ssl context to super().ssl_context or to ssl.create_default_context()? I think removing the following should do? ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONEEiland
yes, removing those two lines would work. (They were removed from Django's SMTP EmailBackend in Django 4.2.)Neglectful
thx again @Neglectful - I changed the answer according to your feedback.Eiland
I
3

The top voted answer helps me solve the issue. Just add this answer with more details.

Thanks to this post.

Reason

This is due to the ssl_context.check_hostname field in the django.core.mail.backends.smtp.py file being set to True by default, starting from Django 4.2.

Solution

In test environment:

In the settings.py:

EMAIL_USE_TLS = False

Note: This should only be used for development. It is important that TLS is enabled for production or any public facing service.

In production environment:

  1. Create a custom email backend to set the check_hostname and verify_mode values.
# file path: backend/email.py
import ssl

from django.core.mail.backends.smtp import EmailBackend as SMTPBackend
from django.utils.functional import cached_property


class EmailBackend(SMTPBackend):
  @cached_property
  def ssl_context(self):
      if self.ssl_certfile or self.ssl_keyfile:
          ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
          ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile)
          return ssl_context
      else:
          ssl_context = ssl.create_default_context()
          ssl_context.check_hostname = False
          ssl_context.verify_mode = ssl.CERT_NONE
          return ssl_context
  1. Use the custom backend rather than the standard one
# settings.py
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # old value
EMAIL_BACKEND = 'backend.email.EmailBackend'
Isoleucine answered 15/1 at 1:43 Comment(1)
This solution disables certificate verification and hostname checking, which isn't very secure. Better choices are loading an updated trust store from certifi, or if you're using a private CA or self-signed cert using load_verify_locations().Neglectful
G
3

This is the solution that worked for me

add the lines below to the settings.py file

import certifi, os

os.environ[‘SSL_CERT_FILE’] = certifi.where()
Girgenti answered 12/4 at 11:54 Comment(0)
S
1

I was using Django 4.2 and Python 3.10, running into the same issue. Downgrading Django to 4.1.7 worked for me. Hope this helps

Stablish answered 17/4, 2023 at 0:31 Comment(1)
Django 4.2 made this change to improve the security of the connection to your SMTP server. Downgrading Django will reduce security. A better choice is using certifi to update your system trust store.Neglectful
R
0

I simply corrected the hostname of the smtp server in settings.py ... with the old version of Django it also worked with its alias, as it did not check the effective hostname... from version 4.2 after doing some tests found in this forum, also working, finally I had an idea, I searched for the real hostname of the smtp and only with that, without needing to do anything else it worked perfectly!

Rotary answered 25/9 at 21:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.