I have only seen one online report of this to date, but perhaps others have run into this and found a way around it. Email delivery using the Python email package has worked in prior versions of Python, but fails in version 3.13. The problem is not with the email package but with SSL.
It appears that version 3.13 of Python has changed the requirement for one of the settings in SSL certificates. And if your email servers certificates do not have this new setting set, your emails deliveries will fail with an SSL failure.
There is a field called Basic Constraints in each certificate, which was ignored in prior versions of Python, but now it looks for that field to be set to Critical. If it is not, SSL declares the handshake as a failed verification. So it throws an exception.
I reviewed the certification that my email server is using for SSL and this field is set to False, not Critical. And since the hosting provider is unable to change a certificate for individual clients, it would be necessary to purchase a custom certification and install it on the sever. This is costly for each domain being used for email delivery.
Have others run into this issue and gotten around it without having to now purchase a custom certification because of the setting that their email service providers has set? Certainly one way around it is to ignore SSL verification, but that does not seem wise.
I am surprised that there would not be a new setting in Python to accept the prior level of certification that v3.12 supported. But we can run the same software on different versions of Python and see the failure on v3.13.
import ssl
EmailServer = 'SomeDomain.com'
EmailPort = 465
context = ssl.create_default_context()
with smtplib.SMTP_SSL (EmailServer, EmailPort, context=context) as server:          
    server.login(EmailServerUsername, EmailServerPassword)
    server.send_message(msg)
In this code segment in V3.13.1, an exception is thrown at the "with smtplib.SMTP_SSL" line and the server.login is not reached.
The exception thrown is: Emailout exception [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Basic Constraints of CA cert not marked critical (_ssl.c:1018)
SOLUTION: In reading the Python 3.13 release notes, I was shown that a flag was set in SSL that required a more strict SSL certificate in order to pass verification. Turning off this 'strict flag' returns SSL to the same level of verification that was in V3.12. So the following line is added after the context = ssl.create_default_context() statement.
context.verify_flags &= ~ssl.VERIFY_X509_STRICT
This is turning off the STRICT flag so the Basic Constraint Critical is not required to pass certification.
Perhaps the title is not exactly correct, but its purpose was to get attention and a suggestion of how to fix the particular issue being faced. It isn't so much as Email being broken as it is the v3.13 increased the level of SSL security from v3.12. But with that title, one might not realize the implication of such a change unless they ran into a email delivery issue as I did. The first comments posted did in fact point to the solution. Unfortunately my hosting provider does not provide certificates as strict in their settings as v3.13 required
There is way to monkey-patch SSL behaviour in Python 3.13+ so it will be "fixed" even for packages that use connection somewhere deep inside.
import ssl
import warnings
warnings.warn(
    "Disabling VERIFY_X509_STRICT and VERIFY_X509_PARTIAL_CHAIN in create_default_context().\n"
    "This reverts Python 3.13's stricter SSL checks. Use only if you cannot fix your CA!"
)
_original_create_default_context = ssl.create_default_context
def relaxed_create_default_context(
    purpose=ssl.Purpose.SERVER_AUTH,
    *,
    cafile=None,
    capath=None,
    cadata=None
):
    # Call the original function
    ctx = _original_create_default_context(purpose=purpose, cafile=cafile, capath=capath, cadata=cadata)
    
    # Remove the Python 3.13 flags:
    #   ssl.VERIFY_X509_STRICT       = 0x10000
    #   ssl.VERIFY_X509_PARTIAL_CHAIN = 0x80000
    if hasattr(ssl, "VERIFY_X509_STRICT"):
        ctx.verify_flags = ctx.verify_flags & ~ssl.VERIFY_X509_STRICT
    if hasattr(ssl, "VERIFY_X509_PARTIAL_CHAIN"):
        ctx.verify_flags = ctx.verify_flags & ~ssl.VERIFY_X509_PARTIAL_CHAIN
    return ctx
# Monkey-patch the built-in function
ssl.create_default_context = relaxed_create_default_context
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With