From e83af8e0bd57991b6f8b6f2b63a678e6d9b24d51 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 21 Sep 2023 12:27:36 -0400 Subject: [PATCH] Add SMTPService and Email helper * Email helper class that wraps EmailMessage * SMTPService class that creates connection to SMTP server using config options in services.smtp * send method on SMTPService that takes Email as an arg to send an email using the SMTP connection Signed-off-by: joseph-sentry --- conftest.py | 6 +++++ helpers/email.py | 13 +++++++++++ services/smtp.py | 44 +++++++++++++++++++++++++++++++++++++ services/tests/test_smtp.py | 26 ++++++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 helpers/email.py create mode 100644 services/smtp.py create mode 100644 services/tests/test_smtp.py diff --git a/conftest.py b/conftest.py index 54ca45107..528df424e 100644 --- a/conftest.py +++ b/conftest.py @@ -134,6 +134,12 @@ def mock_configuration(mocker): "verify_ssl": False, }, "redis_url": "redis://redis:@localhost:6379/", + "smtp": { + "host": "testserver", + "port": 12345, + "username": None, + "password": None, + }, }, "setup": { "codecov_url": "https://codecov.io", diff --git a/helpers/email.py b/helpers/email.py new file mode 100644 index 000000000..db37f6301 --- /dev/null +++ b/helpers/email.py @@ -0,0 +1,13 @@ +from email.message import EmailMessage + + +class Email: + def __init__( + self, to_addr=None, from_addr=None, subject=None, text=None, html=None + ): + self.message = EmailMessage() + self.message["To"] = to_addr + self.message["From"] = from_addr + self.message["Subject"] = subject + self.message.set_content(text) + self.message.add_alternative(html, "text/html") diff --git a/services/smtp.py b/services/smtp.py new file mode 100644 index 000000000..f073a1d0f --- /dev/null +++ b/services/smtp.py @@ -0,0 +1,44 @@ +import smtplib +import ssl +from functools import cached_property + +from shared.config import get_config + +from helpers.email import Email + +_smtp_service = None + + +def get_smtp_service(): + if len(get_config("services", "smtp", default={})) == 0: + return None + return _get_cached_smtp_service() + + +def _get_cached_smtp_service(): + global _smtp_service + if _smtp_service is None: + _smtp_service = SMTPService() + return _smtp_service + + +class SMTPService: + def __init__(self): + self.host = get_config("services", "smtp", "host", default="mailhog") + self.port = get_config("services", "smtp", "port", default=1025) + self.username = get_config("services", "smtp", "username", default=None) + self.password = get_config("services", "smtp", "password", default=None) + self.ssl_context = ssl.create_default_context() + + self._conn = smtplib.SMTP( + host=self.host, + port=self.port, + ) + self._conn.starttls(context=self.ssl_context) + if self.username and self.password: + self._conn.login(self.username, self.password) + + def send(self, email: Email): + return self._conn.send_message( + email.message, + ) diff --git a/services/tests/test_smtp.py b/services/tests/test_smtp.py new file mode 100644 index 000000000..ac57fb8e2 --- /dev/null +++ b/services/tests/test_smtp.py @@ -0,0 +1,26 @@ +from services.smtp import get_smtp_service + +from unittest.mock import call, MagicMock +from ssl import create_default_context + + +class TestStorage(object): + def test_correct_init(self, mocker, mock_configuration): + mocker.patch("smtplib.SMTP") + m = mocker.patch("ssl.create_default_context", return_value=MagicMock()) + service = get_smtp_service() + service._conn.starttls.assert_called_with(context=m.return_value) + + def test_idempotent_service(self, mocker, mock_configuration): + mocker.patch("smtplib.SMTP") + first = get_smtp_service() + second = get_smtp_service() + assert id(first) == id(second) + + def test_idempotent_connection(self, mocker, mock_configuration): + mocker.patch("smtplib.SMTP") + first = get_smtp_service() + first_conn = first._conn + second = get_smtp_service() + second_conn = second._conn + assert id(first_conn) == id(second_conn)