refactor: implement email with stdlib and drop email dependency (#1746)

* implement email with stdlib and drop dependency

* potentially provide in logs
This commit is contained in:
Hayden 2022-10-21 19:12:27 -08:00 committed by GitHub
parent d53e78317d
commit 0bb1b6500f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 71 additions and 115 deletions

View file

@ -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

99
poetry.lock generated
View file

@ -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"},

View file

@ -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"