From 728e35e8611e854d735735054f997df96e5b50a0 Mon Sep 17 00:00:00 2001
From: Emmanuel Ormancey <emmanuel.ormancey@cern.ch>
Date: Wed, 30 Jun 2021 16:07:43 +0200
Subject: [PATCH] Added new smimesmtp backend

---
 .env                                     |   5 +
 notifications_consumer/config.py         |   4 +
 poetry.lock                              |  60 +++++++-
 pyproject.toml                           |   1 +
 vendor/django_mail/backends/smimesmtp.py | 174 +++++++++++++++++++++++
 5 files changed, 243 insertions(+), 1 deletion(-)
 create mode 100644 vendor/django_mail/backends/smimesmtp.py

diff --git a/.env b/.env
index 7be22b1..ee716e3 100644
--- a/.env
+++ b/.env
@@ -31,3 +31,8 @@ CERN_OIDC_CLIENT_SECRET=fill-me
 
 EMAIL_AES_SECRET_KEY=fill-mefill-mefill-mefill-mefill
 
+# SMIME signed emails
+EMAIL_BACKEND=vendor.django_mail.backends.smimesmtp.EmailBackend
+EMAIL_SMIME_CERT_FILE_PATH=/etc/notifications-noreply.pem
+EMAIL_SMIME_CERT_KEY_FILE_PATH=/etc/notifications-noreply-key.pem
+
diff --git a/notifications_consumer/config.py b/notifications_consumer/config.py
index 5c5c207..ee74764 100644
--- a/notifications_consumer/config.py
+++ b/notifications_consumer/config.py
@@ -55,6 +55,10 @@ class Config:
     EMAIL_WHITELIST = ast.literal_eval(os.getenv("EMAIL_WHITELIST", "['user@cern.ch']"))
 
     EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "vendor.django_mail.backends.console.EmailBackend")
+
+    EMAIL_SMIME_CERT_FILE_PATH = os.getenv("EMAIL_SMIME_CERT_FILE_PATH")
+    EMAIL_SMIME_CERT_KEY_FILE_PATH = os.getenv("EMAIL_SMIME_CERT_KEY_FILE_PATH")
+
     NOREPLY_ADDRESS = os.getenv("NOREPLY_ADDRESS", "noreply@cern.ch")
 
     EMAIL_RECIPIENT_REGEX = r"^(.*?)\+(.*?)\+(.*?)$"
diff --git a/poetry.lock b/poetry.lock
index fe4ee4c..bdb95b8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -22,6 +22,14 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "asn1crypto"
+version = "1.4.0"
+description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "atomicwrites"
 version = "1.4.0"
@@ -314,6 +322,17 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "oscrypto"
+version = "1.2.1"
+description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+asn1crypto = ">=1.0.0"
+
 [[package]]
 name = "packaging"
 version = "20.9"
@@ -470,6 +489,21 @@ toml = "*"
 [package.extras]
 testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
 
+[[package]]
+name = "python-smail"
+version = "0.9.0"
+description = "Simple S/MIME e-mails with Python3"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+asn1crypto = "*"
+oscrypto = "*"
+
+[package.extras]
+test = ["coverage", "flake8", "tox"]
+
 [[package]]
 name = "pywebpush"
 version = "1.13.0"
@@ -658,7 +692,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6.1"
-content-hash = "b289528ab61c5d62e64fcab64e19a335ada3683c86b33bb29ed7b23840a47260"
+content-hash = "4ad4f600d58c371c5ee85fd33950f55c212edc0620b969f9c7541246c1800f73"
 
 [metadata.files]
 apns2 = [
@@ -669,6 +703,10 @@ appdirs = [
     {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
     {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
 ]
+asn1crypto = [
+    {file = "asn1crypto-1.4.0-py2.py3-none-any.whl", hash = "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8"},
+    {file = "asn1crypto-1.4.0.tar.gz", hash = "sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c"},
+]
 atomicwrites = [
     {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
     {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -698,24 +736,36 @@ cffi = [
     {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
     {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
     {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"},
     {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
     {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
     {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"},
     {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
     {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
     {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"},
     {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
     {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
     {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"},
     {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
     {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
     {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
@@ -855,6 +905,10 @@ nodeenv = [
     {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
     {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
 ]
+oscrypto = [
+    {file = "oscrypto-1.2.1-py2.py3-none-any.whl", hash = "sha256:988087e05b17df8bfcc7c5fac51f54595e46d3e4dffa7b3d15955cf61a633529"},
+    {file = "oscrypto-1.2.1.tar.gz", hash = "sha256:7d2cca6235d89d1af6eb9cfcd4d2c0cb405849868157b2f7b278beb644d48694"},
+]
 packaging = [
     {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
     {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
@@ -942,6 +996,10 @@ pytest = [
     {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
     {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
 ]
+python-smail = [
+    {file = "python-smail-0.9.0.tar.gz", hash = "sha256:e0da2fea2189a8dece2ab1ea78a670ba2f1d025742ea118b7358e5a9ca2f052f"},
+    {file = "python_smail-0.9.0-py2-none-any.whl", hash = "sha256:b15e085efcf10813c37bb9fc0c1b0d6564a39b30b91c308cd443201942c718e5"},
+]
 pywebpush = [
     {file = "pywebpush-1.13.0.tar.gz", hash = "sha256:97ef000a685cd1f63d9d3553568508508904bfe419485df2b83b025d94e9ae54"},
 ]
diff --git a/pyproject.toml b/pyproject.toml
index 4353990..46da9dd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,6 +19,7 @@ apns2 = "^0.7.2"
 pytest = "^6.2.3"
 unittest = "^0.0"
 pycrypto = "^2.6.1"
+python-smail = "^0.9.0"
 
 [tool.poetry.dev-dependencies]
 pre-commit = "~2.9.2"
diff --git a/vendor/django_mail/backends/smimesmtp.py b/vendor/django_mail/backends/smimesmtp.py
new file mode 100644
index 0000000..02d6f94
--- /dev/null
+++ b/vendor/django_mail/backends/smimesmtp.py
@@ -0,0 +1,174 @@
+# The code is taken from `newdle.newdle.vendor.django_mail.backends.smtp`
+# Minor changes has been done to substitute config values.
+
+# The code in here is taken almost verbatim from `django.core.mail.backends.smtp`,
+# which is licensed under the three-clause BSD license and is originally
+# available on the following URL:
+# https://github.com/django/django/blob/stable/2.2.x/django/core/mail/backends/smtp.py
+# Credits of the original code go to the Django Software Foundation
+# and their contributors.
+
+"""SMTP email backend class."""
+import smtplib
+import socket
+import ssl
+import threading
+
+## SMIME https://pypi.org/project/python-smail/
+from smail import sign_message
+from notifications_consumer.config import Config
+
+from ..mail_utils import DEFAULT_CHARSET, DNS_NAME
+from ..message import sanitize_address
+from .base import BaseEmailBackend
+from vendor.django_mail.message import SafeMIMEMultipart
+
+
+class EmailBackend(BaseEmailBackend):
+    """
+    A wrapper that manages the SMTP network connection.
+    """
+
+    def __init__(
+        self,
+        host=None,
+        port=None,
+        username=None,
+        password=None,
+        use_tls=None,
+        fail_silently=False,
+        use_ssl=None,
+        timeout=None,
+        ssl_keyfile=None,
+        ssl_certfile=None,
+        **kwargs,
+    ):
+        super().__init__(fail_silently=fail_silently)
+        self.host = host or Config.EMAIL_HOST
+        self.port = port or Config.EMAIL_PORT
+        self.username = Config.EMAIL_HOST_USER if username is None else username
+        self.password = (
+            Config.EMAIL_HOST_PASSWORD if password is None else password
+        )
+        self.use_tls = Config.EMAIL_USE_TLS if use_tls is None else use_tls
+        self.use_ssl = Config.EMAIL_USE_SSL if use_ssl is None else use_ssl
+        self.timeout = Config.EMAIL_TIMEOUT if timeout is None else timeout
+        self.ssl_keyfile = ssl_keyfile
+        self.ssl_certfile = ssl_certfile
+        if self.use_ssl and self.use_tls:
+            raise ValueError(
+                "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set "
+                "one of those settings to True."
+            )
+        self.connection = None
+        self._lock = threading.RLock()
+
+    @property
+    def connection_class(self):
+        return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
+
+    def open(self):
+        """
+        Ensure an open connection to the email server. Return whether or not a
+        new connection was required (True or False) or None if an exception
+        passed silently.
+        """
+        if self.connection:
+            # Nothing to do if the connection is already open.
+            return False
+
+        # If local_hostname is not specified, socket.getfqdn() gets used.
+        # For performance, we use the cached FQDN for local_hostname.
+        connection_params = {"local_hostname": DNS_NAME.get_fqdn()}
+        if self.timeout is not None:
+            connection_params["timeout"] = self.timeout
+        if self.use_ssl:
+            connection_params.update(
+                {"keyfile": self.ssl_keyfile, "certfile": self.ssl_certfile}
+            )
+        try:
+            self.connection = self.connection_class(
+                self.host, self.port, **connection_params
+            )
+
+            # TLS/SSL are mutually exclusive, so only attempt TLS over
+            # non-secure connections.
+            if not self.use_ssl and self.use_tls:
+                self.connection.starttls(
+                    keyfile=self.ssl_keyfile, certfile=self.ssl_certfile
+                )
+            if self.username and self.password:
+                self.connection.login(self.username, self.password)
+            return True
+        except (smtplib.SMTPException, socket.error):
+            if not self.fail_silently:
+                raise
+
+    def close(self):
+        """Close the connection to the email server."""
+        if self.connection is None:
+            return
+        try:
+            try:
+                self.connection.quit()
+            except (ssl.SSLError, smtplib.SMTPServerDisconnected):
+                # This happens when calling quit() on a TLS connection
+                # sometimes, or when the connection was already disconnected
+                # by the server.
+                self.connection.close()
+            except smtplib.SMTPException:
+                if self.fail_silently:
+                    return
+                raise
+        finally:
+            self.connection = None
+
+    def send_messages(self, email_messages):
+        """
+        Send one or more EmailMessage objects and return the number of email
+        messages sent.
+        """
+        if not email_messages:
+            return 0
+        with self._lock:
+            new_conn_created = self.open()
+            if not self.connection or new_conn_created is None:
+                # We failed silently on open().
+                # Trying to send would be pointless.
+                return 0
+            num_sent = 0
+            for message in email_messages:
+                sent = self._send(message)
+                if sent:
+                    num_sent += 1
+            if new_conn_created:
+                self.close()
+        return num_sent
+
+    def _send(self, email_message):
+        """A helper method that does the actual sending."""
+        if not email_message.recipients():
+            return False
+        encoding = email_message.encoding or DEFAULT_CHARSET
+        from_email = sanitize_address(email_message.from_email, encoding)
+        recipients = [
+            sanitize_address(addr, encoding) for addr in email_message.recipients()
+        ]
+
+        to_sign = email_message.message()
+        key_signer = Config.EMAIL_SMIME_CERT_KEY_FILE_PATH
+        cert_signer = Config.EMAIL_SMIME_CERT_FILE_PATH
+        signed_message = sign_message(to_sign, key_signer, cert_signer, multipart_class=SafeMIMEMultipart)
+
+        try:
+            self.connection.sendmail(
+                from_email, 
+                recipients,
+                # Ugly fix for obscure boundary with newline when using SafeMIMEMultipart
+                signed_message.as_string().replace('\n', '\r\n').replace('; boundary', ';\r\n boundary') 
+            )
+        except smtplib.SMTPException:
+            if not self.fail_silently:
+                raise
+            return False
+        return True
-- 
GitLab