#!/usr/bin/env python
# cardinal_pythonlib/django/mail.py

"""
===============================================================================

    Original code copyright (C) 2009-2021 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__)


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

    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
