Feat & Fix(API): Sending emails to users

- Send emails to users when they register or when they reset their
  password.
- Fix `.gitignore` not recoginizing changes in `lib` folder
This commit is contained in:
minhtrannhat 2022-12-15 09:40:38 -05:00
parent 69debdb213
commit 61bac977ff
No known key found for this signature in database
GPG Key ID: 894C6A5801E01CA9
10 changed files with 152 additions and 12 deletions

View File

@ -17,22 +17,23 @@
#### Python dependencies #### Python dependencies
- `quart`: a micro-webframework, async version of Flask. - `quart`: a micro-webframework, async version of Flask.
- `black`: Code formatter - `black`: Code formatter.
- `isort`: Import formatter - `isort`: Import formatter.
- `mypy`: Type checking - `mypy`: Type checking.
- `flake8`: General Python bugs - `flake8`: General Python bugs.
- `vulture`: Find unused code in Python programs - `vulture`: Find unused code in Python programs.
- `pytest`: For testing (turbocharged with `async`) - `pytest`: For testing (turbocharged with `async`).
- `bcrypt`: Hashing and salting password. - `bcrypt`: Hashing and salting password.
- `zxcvbn`: Test password strength. - `zxcvbn`: Test password strength.
- `freezegun`: Check for expired token. - `freezegun`: Check for expired token.
- `quart-rate-limiter`: Rate limiting - `quart-rate-limiter`: Rate limiting.
- `pydantic` and `quart-schema`: Request/Response validation - `pydantic` and `quart-schema`: Request/Response validation.
- `httpx`: send HTTP POST requests from our app.
#### SQL Dev-deps #### SQL Dev-deps
- `bandit`: Check for SQL injection vulnerabilities - `bandit`: Check for SQL injection vulnerabilities.
#### Miscs Dev-deps #### Miscs Dev-deps
- `djhtml`: Generate jinja templates html for emails - `djhtml`: Generate jinja templates html for emails.

1
backend/.gitignore vendored
View File

@ -14,7 +14,6 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/

87
backend/pdm.lock generated
View File

@ -4,6 +4,16 @@ version = "22.1.0"
requires_python = ">=3.7,<4.0" requires_python = ">=3.7,<4.0"
summary = "File support for asyncio." summary = "File support for asyncio."
[[package]]
name = "anyio"
version = "3.6.2"
requires_python = ">=3.6.2"
summary = "High level compatibility layer for multiple asynchronous event loop implementations"
dependencies = [
"idna>=2.8",
"sniffio>=1.1",
]
[[package]] [[package]]
name = "asyncpg" name = "asyncpg"
version = "0.27.0" version = "0.27.0"
@ -59,6 +69,12 @@ version = "0.4"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "Query building for the postgresql prepared statements and asyncpg." summary = "Query building for the postgresql prepared statements and asyncpg."
[[package]]
name = "certifi"
version = "2022.12.7"
requires_python = ">=3.6"
summary = "Python package for providing Mozilla's CA Bundle."
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.3" version = "8.1.3"
@ -161,6 +177,30 @@ version = "4.0.0"
requires_python = ">=3.6.1" requires_python = ">=3.6.1"
summary = "Pure-Python HPACK header compression" summary = "Pure-Python HPACK header compression"
[[package]]
name = "httpcore"
version = "0.16.2"
requires_python = ">=3.7"
summary = "A minimal low-level HTTP client."
dependencies = [
"anyio<5.0,>=3.0",
"certifi",
"h11<0.15,>=0.13",
"sniffio==1.*",
]
[[package]]
name = "httpx"
version = "0.23.1"
requires_python = ">=3.7"
summary = "The next generation HTTP client."
dependencies = [
"certifi",
"httpcore<0.17.0,>=0.15.0",
"rfc3986[idna2008]<2,>=1.3",
"sniffio",
]
[[package]] [[package]]
name = "hypercorn" name = "hypercorn"
version = "0.14.3" version = "0.14.3"
@ -427,6 +467,21 @@ dependencies = [
"quart>=0.18.1", "quart>=0.18.1",
] ]
[[package]]
name = "rfc3986"
version = "1.5.0"
summary = "Validating URI References per RFC 3986"
[[package]]
name = "rfc3986"
version = "1.5.0"
extras = ["idna2008"]
summary = "Validating URI References per RFC 3986"
dependencies = [
"idna",
"rfc3986==1.5.0",
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -439,6 +494,12 @@ version = "5.0.0"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "A pure Python implementation of a sliding window memory map manager" summary = "A pure Python implementation of a sliding window memory map manager"
[[package]]
name = "sniffio"
version = "1.3.0"
requires_python = ">=3.7"
summary = "Sniff out which async library your code is running under"
[[package]] [[package]]
name = "stevedore" name = "stevedore"
version = "4.1.1" version = "4.1.1"
@ -495,13 +556,17 @@ dependencies = [
[metadata] [metadata]
lock_version = "4.1" lock_version = "4.1"
content_hash = "sha256:79bba26f32608f765b40874b42c2340957f8241ef3cc7260f145eb26c2474b52" content_hash = "sha256:13ab53060a70e6594c56adc5109a6476e7c085b65be0550d113c28338ef1b985"
[metadata.files] [metadata.files]
"aiofiles 22.1.0" = [ "aiofiles 22.1.0" = [
{url = "https://files.pythonhosted.org/packages/86/26/6e5060a159a6131c430e8a01ec8327405a19a449a506224b394e36f2ebc9/aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"}, {url = "https://files.pythonhosted.org/packages/86/26/6e5060a159a6131c430e8a01ec8327405a19a449a506224b394e36f2ebc9/aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"},
{url = "https://files.pythonhosted.org/packages/a0/48/d5d1ab7cfe46e573c3694fa1365442a7d7cadc3abb03d8507e58a3755bb2/aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"}, {url = "https://files.pythonhosted.org/packages/a0/48/d5d1ab7cfe46e573c3694fa1365442a7d7cadc3abb03d8507e58a3755bb2/aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"},
] ]
"anyio 3.6.2" = [
{url = "https://files.pythonhosted.org/packages/77/2b/b4c0b7a3f3d61adb1a1e0b78f90a94e2b6162a043880704b7437ef297cad/anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
{url = "https://files.pythonhosted.org/packages/8b/94/6928d4345f2bc1beecbff03325cad43d320717f51ab74ab5a571324f4f5a/anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
]
"asyncpg 0.27.0" = [ "asyncpg 0.27.0" = [
{url = "https://files.pythonhosted.org/packages/04/78/06b4979eb2b553a450fe38008353f5cba152a66de83c64b1639046e9ca0e/asyncpg-0.27.0.tar.gz", hash = "sha256:720986d9a4705dd8a40fdf172036f5ae787225036a7eb46e704c45aa8f62c054"}, {url = "https://files.pythonhosted.org/packages/04/78/06b4979eb2b553a450fe38008353f5cba152a66de83c64b1639046e9ca0e/asyncpg-0.27.0.tar.gz", hash = "sha256:720986d9a4705dd8a40fdf172036f5ae787225036a7eb46e704c45aa8f62c054"},
{url = "https://files.pythonhosted.org/packages/05/46/250c9e277b77ccbfd1b1746c529fc61250280f854d686b2775f8e4d59be5/asyncpg-0.27.0-cp39-cp39-win32.whl", hash = "sha256:8934577e1ed13f7d2d9cea3cc016cc6f95c19faedea2c2b56a6f94f257cea672"}, {url = "https://files.pythonhosted.org/packages/05/46/250c9e277b77ccbfd1b1746c529fc61250280f854d686b2775f8e4d59be5/asyncpg-0.27.0-cp39-cp39-win32.whl", hash = "sha256:8934577e1ed13f7d2d9cea3cc016cc6f95c19faedea2c2b56a6f94f257cea672"},
@ -593,6 +658,10 @@ content_hash = "sha256:79bba26f32608f765b40874b42c2340957f8241ef3cc7260f145eb26c
{url = "https://files.pythonhosted.org/packages/31/5a/c5ecd08a0c9b4dfece3b41aeefc3770968b4a2da1784941c9c8dd1c65347/buildpg-0.4-py3-none-any.whl", hash = "sha256:20d539976c81ea6f5529d3930016b0482ed0ff06def3d6da79d0fc0a3bbaeeb1"}, {url = "https://files.pythonhosted.org/packages/31/5a/c5ecd08a0c9b4dfece3b41aeefc3770968b4a2da1784941c9c8dd1c65347/buildpg-0.4-py3-none-any.whl", hash = "sha256:20d539976c81ea6f5529d3930016b0482ed0ff06def3d6da79d0fc0a3bbaeeb1"},
{url = "https://files.pythonhosted.org/packages/48/f2/ff0e51a3c2390538da6eb4f85e30d87aafbcc6d057c6c9bb9fa222c8f2fc/buildpg-0.4.tar.gz", hash = "sha256:3a6c1f40fb6c826caa819d84727e36a1372f7013ba696637b492e5935916d479"}, {url = "https://files.pythonhosted.org/packages/48/f2/ff0e51a3c2390538da6eb4f85e30d87aafbcc6d057c6c9bb9fa222c8f2fc/buildpg-0.4.tar.gz", hash = "sha256:3a6c1f40fb6c826caa819d84727e36a1372f7013ba696637b492e5935916d479"},
] ]
"certifi 2022.12.7" = [
{url = "https://files.pythonhosted.org/packages/37/f7/2b1b0ec44fdc30a3d31dfebe52226be9ddc40cd6c0f34ffc8923ba423b69/certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
{url = "https://files.pythonhosted.org/packages/71/4c/3db2b8021bd6f2f0ceb0e088d6b2d49147671f25832fb17970e9b583d742/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
]
"click 8.1.3" = [ "click 8.1.3" = [
{url = "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, {url = "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
{url = "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {url = "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
@ -644,6 +713,14 @@ content_hash = "sha256:79bba26f32608f765b40874b42c2340957f8241ef3cc7260f145eb26c
{url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, {url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
{url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, {url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
] ]
"httpcore 0.16.2" = [
{url = "https://files.pythonhosted.org/packages/91/52/93f22e5441539256c0d113faf17e45284aee16eebdd95089e3ca6f480b18/httpcore-0.16.2-py3-none-any.whl", hash = "sha256:52c79095197178856724541e845f2db86d5f1527640d9254b5b8f6f6cebfdee6"},
{url = "https://files.pythonhosted.org/packages/9b/20/26f6cc4fd00391f8f1c57b0020f5c6eec23904723db04b6f7608e222d815/httpcore-0.16.2.tar.gz", hash = "sha256:c35c5176dc82db732acfd90b581a3062c999a72305df30c0fc8fafd8e4aca068"},
]
"httpx 0.23.1" = [
{url = "https://files.pythonhosted.org/packages/8a/df/a3e8b91dfb452e645ef110985a30f0915276a1a2144004c7671c07bb203c/httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"},
{url = "https://files.pythonhosted.org/packages/e1/74/cdce73069e021ad5913451b86c2707b027975cf302016ca557686d87eb41/httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"},
]
"hypercorn 0.14.3" = [ "hypercorn 0.14.3" = [
{url = "https://files.pythonhosted.org/packages/2e/e8/2a927f26a0cb994451b07622bf778ac6b3cb9fe45216e13feaeebb27c9b0/Hypercorn-0.14.3.tar.gz", hash = "sha256:4a87a0b7bbe9dc75fab06dbe4b301b9b90416e9866c23a377df21a969d6ab8dd"}, {url = "https://files.pythonhosted.org/packages/2e/e8/2a927f26a0cb994451b07622bf778ac6b3cb9fe45216e13feaeebb27c9b0/Hypercorn-0.14.3.tar.gz", hash = "sha256:4a87a0b7bbe9dc75fab06dbe4b301b9b90416e9866c23a377df21a969d6ab8dd"},
{url = "https://files.pythonhosted.org/packages/be/8b/e856965857137bc594e8fd52fef1d00b79913a6a01033cba772276aff10b/Hypercorn-0.14.3-py3-none-any.whl", hash = "sha256:7c491d5184f28ee960dcdc14ab45d14633ca79d72ddd13cf4fcb4cb854d679ab"}, {url = "https://files.pythonhosted.org/packages/be/8b/e856965857137bc594e8fd52fef1d00b79913a6a01033cba772276aff10b/Hypercorn-0.14.3-py3-none-any.whl", hash = "sha256:7c491d5184f28ee960dcdc14ab45d14633ca79d72ddd13cf4fcb4cb854d679ab"},
@ -906,6 +983,10 @@ content_hash = "sha256:79bba26f32608f765b40874b42c2340957f8241ef3cc7260f145eb26c
{url = "https://files.pythonhosted.org/packages/dd/f3/f245c3bb75d0e166a87ff458ebc89489c210d235de5c50db4ef397295c79/quart-schema-0.14.3.tar.gz", hash = "sha256:c3105d61bf4fe9d8db30dc4fd9bdcc163fa739bf818c5d22537a89590b442fc7"}, {url = "https://files.pythonhosted.org/packages/dd/f3/f245c3bb75d0e166a87ff458ebc89489c210d235de5c50db4ef397295c79/quart-schema-0.14.3.tar.gz", hash = "sha256:c3105d61bf4fe9d8db30dc4fd9bdcc163fa739bf818c5d22537a89590b442fc7"},
{url = "https://files.pythonhosted.org/packages/ff/4d/8f646a8b7becb325311029fbc7b9d6bfd2dec7aab0ef675dffbcdba8efd1/quart_schema-0.14.3-py3-none-any.whl", hash = "sha256:538e4d2b7da93ad73cd0fa6301415c907f23ffb00d5772a8f802a18ba62cfc33"}, {url = "https://files.pythonhosted.org/packages/ff/4d/8f646a8b7becb325311029fbc7b9d6bfd2dec7aab0ef675dffbcdba8efd1/quart_schema-0.14.3-py3-none-any.whl", hash = "sha256:538e4d2b7da93ad73cd0fa6301415c907f23ffb00d5772a8f802a18ba62cfc33"},
] ]
"rfc3986 1.5.0" = [
{url = "https://files.pythonhosted.org/packages/79/30/5b1b6c28c105629cc12b33bdcbb0b11b5bb1880c6cfbd955f9e792921aa8/rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
{url = "https://files.pythonhosted.org/packages/c4/e5/63ca2c4edf4e00657584608bee1001302bbf8c5f569340b78304f2f446cb/rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
]
"six 1.16.0" = [ "six 1.16.0" = [
{url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
{url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@ -914,6 +995,10 @@ content_hash = "sha256:79bba26f32608f765b40874b42c2340957f8241ef3cc7260f145eb26c
{url = "https://files.pythonhosted.org/packages/21/2d/39c6c57032f786f1965022563eec60623bb3e1409ade6ad834ff703724f3/smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, {url = "https://files.pythonhosted.org/packages/21/2d/39c6c57032f786f1965022563eec60623bb3e1409ade6ad834ff703724f3/smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
{url = "https://files.pythonhosted.org/packages/6d/01/7caa71608bc29952ae09b0be63a539e50d2484bc37747797a66a60679856/smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {url = "https://files.pythonhosted.org/packages/6d/01/7caa71608bc29952ae09b0be63a539e50d2484bc37747797a66a60679856/smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
] ]
"sniffio 1.3.0" = [
{url = "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{url = "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
"stevedore 4.1.1" = [ "stevedore 4.1.1" = [
{url = "https://files.pythonhosted.org/packages/66/c0/26afabea111a642f33cfd15f54b3dbe9334679294ad5c0423c556b75eba2/stevedore-4.1.1.tar.gz", hash = "sha256:7f8aeb6e3f90f96832c301bff21a7eb5eefbe894c88c506483d355565d88cc1a"}, {url = "https://files.pythonhosted.org/packages/66/c0/26afabea111a642f33cfd15f54b3dbe9334679294ad5c0423c556b75eba2/stevedore-4.1.1.tar.gz", hash = "sha256:7f8aeb6e3f90f96832c301bff21a7eb5eefbe894c88c506483d355565d88cc1a"},
{url = "https://files.pythonhosted.org/packages/74/a3/72792236f981aee8bb1ef15e72a6f65444150834830f6a97178fe1e2cdf4/stevedore-4.1.1-py3-none-any.whl", hash = "sha256:aa6436565c069b2946fe4ebff07f5041e0c8bf18c7376dd29edf80cf7d524e4e"}, {url = "https://files.pythonhosted.org/packages/74/a3/72792236f981aee8bb1ef15e72a6f65444150834830f6a97178fe1e2cdf4/stevedore-4.1.1-py3-none-any.whl", hash = "sha256:aa6436565c069b2946fe4ebff07f5041e0c8bf18c7376dd29edf80cf7d524e4e"},

View File

@ -16,6 +16,7 @@ dependencies = [
"pydantic[email]>=1.10.2", "pydantic[email]>=1.10.2",
"quart-schema>=0.14.3", "quart-schema>=0.14.3",
"quart-db[postgresql]>=0.4.1", "quart-db[postgresql]>=0.4.1",
"httpx>=0.23.1",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

View File

@ -0,0 +1,4 @@
class APIError(Exception):
def __init__(self, status_code: int, code: str) -> None:
self.status_code = status_code
self.code = code

View File

@ -0,0 +1,40 @@
import logging
from typing import Any, cast
import httpx
from quart import current_app, render_template
log = logging.getLogger(__name__)
class PostmarkError(Exception):
def __init__(self, error_code: int, message: str) -> None:
self.error_code = error_code
self.message = message
async def send_email(
to: str,
subject: str,
template: str,
ctx: dict[str, Any],
) -> None:
content = await render_template(template, **ctx)
log.info("Sending %s to %s\n%s", template, to, content)
token = current_app.config.get("POSTMARK_TOKEN")
if token is not None:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.postmarkapp.com/email",
json={
"From": "Tozo <help@tozo.dev>",
"To": to,
"Subject": subject,
"Tag": template,
"HtmlBody": content,
},
headers={"X-Postmark-Server-Token": token},
)
data = cast(dict, response.json())
if response.status_code != 200:
raise PostmarkError(data["ErrorCode"], data["Message"])

View File

View File

View File

@ -0,0 +1,10 @@
from pytest import LogCaptureFixture
from quart import Quart
from backend.lib.email import send_email
async def test_send_email(app: Quart, caplog: LogCaptureFixture) -> None:
async with app.app_context():
await send_email("member@tozo.dev", "Welcome", "email.html", {})
assert "Sending email.html to member@tozo.dev" in caplog.text