From a1b3d23c37d844921441df74e56412b5209c9b85 Mon Sep 17 00:00:00 2001 From: Aykhan Date: Mon, 11 Sep 2023 21:58:24 +0400 Subject: [PATCH] Added send-email feature --- config/app.env.example | 8 +++++- src/app/core/config.py | 18 ++++++++++-- src/app/main.py | 24 ---------------- src/app/schemas/main.py | 14 ++++++++++ src/app/templates/index.html | 10 +++---- src/app/utils/email.py | 37 +++++++++++++++++++++++++ src/app/views/routers/main.py | 32 +++++++++++++++++++-- src/migrations/env.py | 3 -- src/poetry.lock | 52 ++++++++++++++++++++++++++++++++++- src/pyproject.toml | 1 + 10 files changed, 161 insertions(+), 38 deletions(-) create mode 100644 src/app/schemas/main.py create mode 100644 src/app/utils/email.py diff --git a/config/app.env.example b/config/app.env.example index 946cc15..5f18dd1 100644 --- a/config/app.env.example +++ b/config/app.env.example @@ -1 +1,7 @@ -SECRET_KEY="SECRET_KEY" \ No newline at end of file +SECRET_KEY="SECRET_KEY" +SMTP_HOST="smtp.gmail.com" +SMTP_PORT=587 +SMTP_SSL_TLS=False +SMTP_USER="SMTP_USER" +SMTP_PASSWORD="SMTP_PASSWORD" +EMAIL_RECIPIENTS=["EMAIL_RECIPIENT", "EMAIL_RECIPIENT"] \ No newline at end of file diff --git a/src/app/core/config.py b/src/app/core/config.py index ba51c83..edb0b60 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -1,7 +1,12 @@ -from pydantic import PostgresDsn -from pydantic_settings import BaseSettings +from typing import Optional from pathlib import Path +from pydantic_settings import BaseSettings +from pydantic import ( + EmailStr, + PostgresDsn +) + class Settings(BaseSettings): PROJECT_NAME: str = 'FastAPI Portfolio & Blog' @@ -36,5 +41,14 @@ class Settings(BaseSettings): path=self.POSTGRES_DB ) + SMTP_SSL_TLS: bool = True + SMTP_PORT: int = 587 + SMTP_HOST: str = "smtp.gmail.com" + SMTP_USER: EmailStr + SMTP_PASSWORD: str + + EMAILS_FROM_NAME: str = PROJECT_NAME + EMAIL_RECIPIENTS: list[EmailStr] = [] + settings = Settings() \ No newline at end of file diff --git a/src/app/main.py b/src/app/main.py index cdb03d3..03c25a8 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -40,30 +40,6 @@ app = FastAPI( app.include_router(main_router) -# templates = Jinja2Templates(directory=main_path / 'templates') - - - -# @app.get("/", response_class=HTMLResponse) -# async def read_item(request: Request): -# return templates.TemplateResponse("test.html", {"request": request}) - -# from pydantic import BaseModel - - -# class User(BaseModel): -# username: str -# password: str - -# @app.post("/", response_class=HTMLResponse) -# async def read_item(request: Request, user: User): -# print(user) -# return templates.TemplateResponse("index.html", {"request": request}) - -# @app.get("/t", response_class=HTMLResponse) -# async def read_item(request: Request): -# return templates.TemplateResponse("index.html", {"request": request}) - @app.exception_handler(ValidationError) async def validation_exception_handler(request, exc): diff --git a/src/app/schemas/main.py b/src/app/schemas/main.py new file mode 100644 index 0000000..97534d4 --- /dev/null +++ b/src/app/schemas/main.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Optional + +from fastapi import Form + +from pydantic import EmailStr + + +@dataclass +class SendEmail: + name: str = Form(..., max_length=50) + email: EmailStr = Form(...) + phone: Optional[str] = Form(None, max_length=20) + message: str = Form(..., max_length=1000) \ No newline at end of file diff --git a/src/app/templates/index.html b/src/app/templates/index.html index 62dc2f0..0fbcf6f 100644 --- a/src/app/templates/index.html +++ b/src/app/templates/index.html @@ -56,18 +56,18 @@

Contact me

Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!

-
+
-
+
-
+
-
+
-
+
diff --git a/src/app/utils/email.py b/src/app/utils/email.py new file mode 100644 index 0000000..b1edbd4 --- /dev/null +++ b/src/app/utils/email.py @@ -0,0 +1,37 @@ +from functools import partial + +from fastapi_mail import ( + FastMail, + MessageSchema, + ConnectionConfig, + MessageType +) + +from app.core.config import settings + + +def send_email_notification( + subject: str, + body: str +) -> partial: + + if settings.EMAIL_RECIPIENTS: + conf = ConnectionConfig( + MAIL_USERNAME = settings.SMTP_USER, + MAIL_PASSWORD = settings.SMTP_PASSWORD, + MAIL_FROM = settings.SMTP_USER, + MAIL_PORT = settings.SMTP_PORT, + MAIL_SERVER = settings.SMTP_HOST, + MAIL_SSL_TLS = settings.SMTP_SSL_TLS, + MAIL_STARTTLS = True + ) + + message = MessageSchema( + subject = subject, + recipients = settings.EMAIL_RECIPIENTS, + body = body, + subtype=MessageType.plain + ) + + fast_mail = FastMail(conf) + return partial(fast_mail.send_message, message) \ No newline at end of file diff --git a/src/app/views/routers/main.py b/src/app/views/routers/main.py index 43bde96..7cb325b 100644 --- a/src/app/views/routers/main.py +++ b/src/app/views/routers/main.py @@ -1,12 +1,13 @@ from typing import Annotated from fastapi.templating import Jinja2Templates -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi import ( APIRouter, Query, Request, - Depends + Depends, + BackgroundTasks ) from sqlalchemy.ext.asyncio import AsyncSession @@ -14,6 +15,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app import crud from app.core.config import settings from app.schemas import ListPostInTemplate +from app.schemas.main import SendEmail +from app.utils.email import send_email_notification from app.views.depends import get_async_db @@ -30,6 +33,31 @@ async def home(request: Request): ) +@router.post('/send-email') +async def send_email( + request: Request, + background_tasks: BackgroundTasks, + form_data: SendEmail = Depends() +): + + body = f"name: {form_data.name}\n"\ + f"email: {form_data.email}\n"\ + f"phone: {form_data.phone}\n"\ + f"message: {form_data.message}" + + background_tasks.add_task( + send_email_notification( + subject = f"Portfolio Blog (by {form_data.email})", + body = body + ) + ) + + return RedirectResponse( + str(request.url_for('home')) + '#contact', + status_code=303 + ) + + @router.get('/blog', response_class=HTMLResponse) async def blog( request: Request, diff --git a/src/migrations/env.py b/src/migrations/env.py index 644ff9c..774d0cc 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -23,9 +23,6 @@ if config.config_file_name is not None: # target_metadata = mymodel.Base.metadata ########### Custom Config ########### -from os import getenv -print('-'*100) -print(getenv('POSTGRES_USER')) from app.core.config import settings from app.db.base import Base diff --git a/src/poetry.lock b/src/poetry.lock index 9b5da1a..32a499d 100644 --- a/src/poetry.lock +++ b/src/poetry.lock @@ -11,6 +11,21 @@ files = [ {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, ] +[[package]] +name = "aiosmtplib" +version = "2.0.2" +description = "asyncio SMTP client" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "aiosmtplib-2.0.2-py3-none-any.whl", hash = "sha256:1e631a7a3936d3e11c6a144fb8ffd94bb4a99b714f2cb433e825d88b698e37bc"}, + {file = "aiosmtplib-2.0.2.tar.gz", hash = "sha256:138599a3227605d29a9081b646415e9e793796ca05322a78f69179f0135016a3"}, +] + +[package.extras] +docs = ["sphinx (>=5.3.0,<6.0.0)", "sphinx_autodoc_typehints (>=1.7.0,<2.0.0)"] +uvloop = ["uvloop (>=0.14,<0.15)", "uvloop (>=0.14,<0.15)", "uvloop (>=0.17,<0.18)"] + [[package]] name = "alembic" version = "1.12.0" @@ -115,6 +130,17 @@ files = [ docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"] +[[package]] +name = "blinker" +version = "1.6.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.7" +files = [ + {file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"}, + {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, +] + [[package]] name = "cffi" version = "1.15.1" @@ -347,6 +373,30 @@ typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "fastapi-mail" +version = "1.4.1" +description = "Simple lightweight mail library for FastApi" +optional = false +python-versions = ">=3.8.1,<4.0" +files = [ + {file = "fastapi_mail-1.4.1-py3-none-any.whl", hash = "sha256:fa5ef23b2dea4d3ba4587f4bbb53f8f15274124998fb4e40629b3b636c76c398"}, + {file = "fastapi_mail-1.4.1.tar.gz", hash = "sha256:9095b713bd9d3abb02fe6d7abb637502aaf680b52e177d60f96273ef6bc8bb70"}, +] + +[package.dependencies] +aiosmtplib = ">=2.0,<3.0" +blinker = ">=1.5,<2.0" +email-validator = ">=2.0,<3.0" +Jinja2 = ">=3.0,<4.0" +pydantic = ">=2.0,<3.0" +pydantic_settings = ">=2.0,<3.0" +starlette = ">=0.24,<1.0" + +[package.extras] +httpx = ["httpx[httpx] (>=0.23,<0.24)"] +redis = ["redis[redis] (>=4.3,<5.0)"] + [[package]] name = "greenlet" version = "2.0.2" @@ -1064,4 +1114,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "98704ea52451d766de4392fa5dc14eaa8eb336758ae4e74a7ecc618439b5af17" +content-hash = "333a9be3573863da41b6a2615cd28aa3a6daf2e608a76da1476af72d343dae52" diff --git a/src/pyproject.toml b/src/pyproject.toml index 2302733..cbc95aa 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -22,6 +22,7 @@ python-slugify = "^8.0.1" pillow = "^10.0.0" aiofiles = "^23.2.1" python-jose = {extras = ["cryptography"], version = "^3.3.0"} +fastapi-mail = "^1.4.1" [build-system]