From 0bb1b6500ff6afe3dcf62cdc81d9a93197bcc2a6 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 21 Oct 2022 19:12:27 -0800 Subject: [PATCH] refactor: implement email with stdlib and drop email dependency (#1746) * implement email with stdlib and drop dependency * potentially provide in logs --- mealie/services/email/email_senders.py | 86 +++++++++++++++++----- poetry.lock | 99 +------------------------- pyproject.toml | 1 - 3 files changed, 71 insertions(+), 115 deletions(-) diff --git a/mealie/services/email/email_senders.py b/mealie/services/email/email_senders.py index 4bdb5707473f..25f9930429ec 100644 --- a/mealie/services/email/email_senders.py +++ b/mealie/services/email/email_senders.py @@ -1,12 +1,55 @@ +import smtplib +import typing from abc import ABC, abstractmethod +from dataclasses import dataclass +from email import message -import emails -from emails.backend.response import SMTPResponse - -from mealie.core.root_logger import get_logger from mealie.services._base_service import BaseService -logger = get_logger() + +@dataclass(slots=True) +class EmailOptions: + host: str + port: int + username: str = None + password: str = None + tls: bool = False + ssl: bool = False + + +@dataclass(slots=True) +class SMTPResponse: + success: bool + message: str + errors: typing.Any + + +@dataclass(slots=True) +class Message: + subject: str + html: str + mail_from: tuple[str, str] + + def send(self, to: str, smtp: EmailOptions) -> SMTPResponse: + msg = message.EmailMessage() + msg["Subject"] = self.subject + msg["From"] = self.mail_from + msg["To"] = to + msg.add_alternative(self.html, subtype="html") + + if smtp.ssl: + with smtplib.SMTP_SSL(smtp.host, smtp.port) as server: + server.login(smtp.username, smtp.password) + errors = server.send_message(msg) + else: + with smtplib.SMTP(smtp.host, smtp.port) as server: + if smtp.tls: + server.starttls() + if smtp.username and smtp.password: + server.login(smtp.username, smtp.password) + errors = server.send_message(msg) + + return SMTPResponse(errors == {}, "Message Sent", errors=errors) class ABCEmailSender(ABC): @@ -16,26 +59,35 @@ class ABCEmailSender(ABC): class DefaultEmailSender(ABCEmailSender, BaseService): + """ + DefaultEmailSender is the default email sender for Mealie. It uses the SMTP settings + from the config file to send emails via the python standard library. It supports + both TLS and SSL connections. + """ + def send(self, email_to: str, subject: str, html: str) -> bool: - message = emails.Message( + message = Message( subject=subject, html=html, mail_from=(self.settings.SMTP_FROM_NAME, self.settings.SMTP_FROM_EMAIL), ) - smtp_options: dict[str, str | bool] = {"host": self.settings.SMTP_HOST, "port": self.settings.SMTP_PORT} - if self.settings.SMTP_AUTH_STRATEGY.upper() == "TLS": - smtp_options["tls"] = True - if self.settings.SMTP_AUTH_STRATEGY.upper() == "SSL": - smtp_options["ssl"] = True + smtp_options = EmailOptions( + self.settings.SMTP_HOST, + int(self.settings.SMTP_PORT), + tls=self.settings.SMTP_AUTH_STRATEGY.upper() == "TLS", + ssl=self.settings.SMTP_AUTH_STRATEGY.upper() == "SSL", + ) + if self.settings.SMTP_USER: - smtp_options["user"] = self.settings.SMTP_USER + smtp_options.username = self.settings.SMTP_USER if self.settings.SMTP_PASSWORD: - smtp_options["password"] = self.settings.SMTP_PASSWORD - response: SMTPResponse = message.send(to=email_to, smtp=smtp_options) - logger.info(f"send email result: {response}") + smtp_options.password = self.settings.SMTP_PASSWORD + + response = message.send(to=email_to, smtp=smtp_options) + self.logger.info(f"send email result: {response}") if not response.success: - logger.error(f"send email error: {response.error}") + self.logger.error(f"send email error: {response}") - return response.status_code in [250] + return response.success diff --git a/poetry.lock b/poetry.lock index 5af5c7374310..64188bb0c684 100644 --- a/poetry.lock +++ b/poetry.lock @@ -161,14 +161,6 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "cachetools" -version = "5.2.0" -description = "Extensible memoizing collections and decorators" -category = "main" -optional = false -python-versions = "~=3.7" - [[package]] name = "certifi" version = "2022.6.15" @@ -196,14 +188,6 @@ category = "dev" optional = false python-versions = ">=3.6.1" -[[package]] -name = "chardet" -version = "5.0.0" -description = "Universal encoding detector for Python 3" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "charset-normalizer" version = "2.1.0" @@ -265,26 +249,6 @@ python-versions = ">=3.6.3,<4.0.0" click = ">=7.1.2" coverage = ">=5.5" -[[package]] -name = "cssselect" -version = "1.1.0" -description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "cssutils" -version = "2.4.2" -description = "A CSS Cascading Style Sheets library for Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] -testing = ["cssselect", "importlib-resources", "jaraco.test (>=5.1)", "lxml", "mock", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - [[package]] name = "dill" version = "0.3.5.1" @@ -319,22 +283,6 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] -[[package]] -name = "emails" -version = "0.6" -description = "Modern python library for emails." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -chardet = "*" -cssutils = "*" -lxml = "*" -premailer = "*" -python-dateutil = "*" -requests = "*" - [[package]] name = "extruct" version = "0.13.0" @@ -918,25 +866,6 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" -[[package]] -name = "premailer" -version = "3.10.0" -description = "Turns CSS blocks into style attributes" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -cachetools = "*" -cssselect = "*" -cssutils = "*" -lxml = "*" -requests = "*" - -[package.extras] -dev = ["black", "flake8", "therapist", "tox", "twine", "wheel"] -test = ["mock", "nose"] - [[package]] name = "psycopg2-binary" version = "2.9.3" @@ -1156,7 +1085,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" @@ -1676,7 +1605,7 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "0beb3c09adcd4531955bc80e6d48a6cafd0b7c25d3310a78c568633a9acaafda" +content-hash = "1d7f2c0315f317738d7976796e88f63468bc98068cf8db5772da8ca1b28fc0e4" [metadata.files] aiofiles = [ @@ -1735,10 +1664,6 @@ black = [ {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] -cachetools = [ - {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, - {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, -] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, @@ -1813,10 +1738,6 @@ cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -chardet = [ - {file = "chardet-5.0.0-py3-none-any.whl", hash = "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557"}, - {file = "chardet-5.0.0.tar.gz", hash = "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa"}, -] charset-normalizer = [ {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, @@ -1891,14 +1812,6 @@ coveragepy-lcov = [ {file = "coveragepy-lcov-0.1.2.tar.gz", hash = "sha256:db6ad0d255d3a8041d30e797a9ec69c77c5963694a6b02a00907b56db7b882a3"}, {file = "coveragepy_lcov-0.1.2-py3-none-any.whl", hash = "sha256:7c1e454ada324a1f47fd7cd2de2c6349b9822cc79691b313ed10370e7ce1b08b"}, ] -cssselect = [ - {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, - {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, -] -cssutils = [ - {file = "cssutils-2.4.2-py3-none-any.whl", hash = "sha256:17e5ba0de70a672cd1cd2de47fd756bd6bce12585acd91447bde7be1d7a6c5c2"}, - {file = "cssutils-2.4.2.tar.gz", hash = "sha256:877818bfa9668cc535773f46e6b6a46de28436191211741b3f7b4aaa64d9afbb"}, -] dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, @@ -1911,10 +1824,6 @@ ecdsa = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, ] -emails = [ - {file = "emails-0.6-py2.py3-none-any.whl", hash = "sha256:72c1e3198075709cc35f67e1b49e2da1a2bc087e9b444073db61a379adfb7f3c"}, - {file = "emails-0.6.tar.gz", hash = "sha256:a4c2d67ea8b8831967a750d8edc6e77040d7693143fe280e6d2a367d9c36ff88"}, -] extruct = [ {file = "extruct-0.13.0-py2.py3-none-any.whl", hash = "sha256:fe19b9aefdb4dfbf828c2b082b81a363a03a44c7591c2d6b62ca225cb8f8c0be"}, {file = "extruct-0.13.0.tar.gz", hash = "sha256:50a5b5bac4c5e19ecf682bf63a28fde0b1bb57433df7057371f60b58c94a2c64"}, @@ -2421,10 +2330,6 @@ pre-commit = [ {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, ] -premailer = [ - {file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"}, - {file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"}, -] psycopg2-binary = [ {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, diff --git a/pyproject.toml b/pyproject.toml index 4f950c384697..228b4512f1f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ apprise = "^0.9.6" recipe-scrapers = "^14.20.0" psycopg2-binary = {version = "^2.9.1", optional = true} gunicorn = "^20.1.0" -emails = "^0.6" python-ldap = "^3.3.1" pydantic = "^1.10.2" tzdata = "^2021.5"