diff --git a/.env.dist b/.env.dist index bcccc38..ea0839d 100644 --- a/.env.dist +++ b/.env.dist @@ -7,3 +7,8 @@ MYSQL_DATABASE: "poletani_cz" MYSQL_USER: "mysql_user" MYSQL_PASSWORD: "****" APP_ENV: "production" + +SMTP_HOST: "" +SMTP_PORT: "" +SMTP_USERNAME: "" +SMTP_PASSWORD: "" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 02b55cd..74f0f2c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ __pycache__/ # app specific files and dirs uploads/* !uploads/.gitkeep -src/emails/templates/html/* \ No newline at end of file +src/emails/templates/html/* +.env diff --git a/docker-compose.yml b/docker-compose.yml index 2ff6426..8947d44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,8 @@ services: - 8000:8000 volumes: - .:/app + env_file: + - .env environment: TZ: "Europe/Prague" API_URL: "http://localhost:8000" diff --git a/requirements.txt b/requirements.txt index 319c3b3..2f311bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ fastapi==0.109.0 fastapi-jwt==0.2.0 +fastapi-mail==1.4.1 strawberry-graphql[fastapi]==0.217.1 uvicorn==0.26.0 sqlalchemy[asyncio] == 2.0.25 diff --git a/src/background_jobs/send_email.py b/src/background_jobs/send_email.py index b8d9334..ae2857b 100644 --- a/src/background_jobs/send_email.py +++ b/src/background_jobs/send_email.py @@ -1,2 +1,7 @@ -async def send_email(email): - pass \ No newline at end of file +from emails.emails import Email +from mailer import Mailer + + +async def send_email(email: Email): + mailer = Mailer() + return await mailer.send(email) \ No newline at end of file diff --git a/src/config.py b/src/config.py index 5d3e42f..7d995cc 100644 --- a/src/config.py +++ b/src/config.py @@ -11,5 +11,10 @@ SENTRY_DSN = os.environ.get("SENTRY_DSN") APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") or "test" +SMTP_HOST = os.environ.get("SMTP_HOST") +SMTP_PORT = int(os.environ.get("SMTP_PORT") or 0) +SMTP_USERNAME = os.environ.get("SMTP_USERNAME") +SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD") + if not APP_SECRET_KEY: raise ValueError("Missing APP_SECRET_KEY!") diff --git a/src/emails/emails.py b/src/emails/emails.py new file mode 100644 index 0000000..5c105b7 --- /dev/null +++ b/src/emails/emails.py @@ -0,0 +1,49 @@ +from typing import Any +from fastapi_mail import MessageSchema, MessageType +from pydantic import EmailStr + + +class Email(MessageSchema): + template: str + + def __init__(self, to: list[EmailStr | str], subject: str, template: str, template_variables: dict, **data: Any) -> None: + data['subtype'] = MessageType.html + data['recipients'] = to + data['template'] = template + data['template_body'] = template_variables + data['subject'] = subject + " | Polétání.cz" + super().__init__(**data) + + +class ContactFormEmail(Email): + def __init__(self, email_from: str, content: str): + super().__init__( + to=["michal@kvacek.cz"], + reply_to=[email_from], + subject="Zpráva z kontaktního formuláře", + template="contact_form.html", + template_variables={ + "content": content, + "email_from": email_from + } + ) + + +class WelcomeEmail(Email): + def __init__(self, to: list[EmailStr | str]): + super().__init__( + to=to, + subject="Vítej", + template="welcome.html", + template_variables={} + ) + + +class ResetPasswordEmail(Email): + def __init__(self, to: list[EmailStr | str], template_variables: dict): + super().__init__( + to=to, + subject="Reset hesla", + template="reset_password.html", + template_variables=template_variables + ) diff --git a/src/emails/welcome.py b/src/emails/welcome.py deleted file mode 100644 index ab7412b..0000000 --- a/src/emails/welcome.py +++ /dev/null @@ -1,39 +0,0 @@ -class Email: - def __init__(self, subject: str, to: str, template: str, template_variables: dict): - self.subject = subject + " | Polétání.cz" - self.to = to - self.template = template - self.template_variables = template_variables - - def get_template_content(self) -> str: - return "" - - -class ContactFormEmail(Email): - def __init__(self, template_variables: dict): - super().__init__( - subject="Zpráva z kontaktního formuláře", - to="michal@kvacek.cz", - template="contact_form.html", - template_variables=template_variables - ) - - -class WelcomeEmail(Email): - def __init__(self, to: str, template_variables: dict): - super().__init__( - subject="Vítej", - to=to, - template="welcome.html", - template_variables=template_variables - ) - - -class ResetPasswordEmail(Email): - def __init__(self, to: str, template_variables: dict): - super().__init__( - subject="Reset hesla", - to=to, - template="reset_password.html", - template_variables=template_variables - ) diff --git a/src/endpoints/contact.py b/src/endpoints/contact.py new file mode 100644 index 0000000..0412cd7 --- /dev/null +++ b/src/endpoints/contact.py @@ -0,0 +1,20 @@ +from fastapi import BackgroundTasks +from pydantic import BaseModel +from starlette.responses import Response +from background_jobs.send_email import send_email +from emails.emails import WelcomeEmail, ContactFormEmail + + +class ContactInput(BaseModel): + email_from: str + content: str + + +class ContactEndpoint(): + async def on_post(self, input: ContactInput, resp: Response, backgorund_tasks: BackgroundTasks): + backgorund_tasks.add_task(send_email, ContactFormEmail( + email_from=input.email_from, + content=input.content + )) + + # todo vratit nejakou smysluplnou diff --git a/src/mailer.py b/src/mailer.py new file mode 100644 index 0000000..9ace2cd --- /dev/null +++ b/src/mailer.py @@ -0,0 +1,22 @@ +from fastapi_mail import ConnectionConfig, FastMail +from config import SMTP_USERNAME, SMTP_PASSWORD, SMTP_PORT, SMTP_HOST +from emails.emails import Email + + +class Mailer: + def __init__(self): + self.conf = ConnectionConfig( + MAIL_USERNAME=SMTP_USERNAME, + MAIL_PASSWORD=SMTP_PASSWORD, + MAIL_FROM_NAME="Polétání", + MAIL_PORT=SMTP_PORT, + MAIL_SERVER=SMTP_HOST, + MAIL_STARTTLS=False, + MAIL_SSL_TLS=True, + USE_CREDENTIALS=True, + TEMPLATE_FOLDER='/app/src/emails/templates/html' + ) + + async def send(self, mail: Email): + fm = FastMail(self.conf) + await fm.send_message(mail, mail.template) diff --git a/src/main.py b/src/main.py index d58b232..3949466 100644 --- a/src/main.py +++ b/src/main.py @@ -12,11 +12,14 @@ from starlette.staticfiles import StaticFiles from strawberry.fastapi import GraphQLRouter from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS from database import models, async_session +from emails.emails import WelcomeEmail +from endpoints.contact import ContactEndpoint, ContactInput from endpoints.forgotten_password import ForgottenPasswordEndpoint from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint from endpoints.photo_editor_preview import PhotoEditorEndpoint from endpoints.registration import RegistrationInput, RegistrationEndpoint from graphql_schema.schema import schema, GraphQLContext +from mailer import Mailer class App: @@ -125,6 +128,14 @@ class App: refresh_token=self.refresh_security ).on_post(user, resp) + @self.api_router.post("/contact") + async def contact(resp: Response, input: ContactInput, background_tasks: BackgroundTasks): + return await ContactEndpoint().on_post( + input, + resp, + background_tasks + ) + @self.api_router.post("/logout") async def logout(resp: Response): return await LogoutEndpoint( @@ -132,10 +143,6 @@ class App: refresh_token=self.refresh_security ).on_post(resp) - # @self.api_router.get("/forgotten-password") - # async def logout(resp: Response): - # return await ForgottenPasswordEndpoint().on_get(resp) - @self.api_router.get("/photo/editor-preview/{photo_id}") async def photo_editor_preview( photo_id: int,