diff --git a/mealie/lang/messages/en-US.json b/mealie/lang/messages/en-US.json index 3d641cf6683..57aa0572eda 100644 --- a/mealie/lang/messages/en-US.json +++ b/mealie/lang/messages/en-US.json @@ -7,7 +7,7 @@ "recipe-defaults": { "ingredient-note": "1 Cup Flour", "step-text": "Recipe steps as well as other fields in the recipe page support markdown syntax.\n\n**Add a link**\n\n[My Link](https://demo.mealie.io)\n" - } + } }, "mealplan": { "no-recipes-match-your-rules": "No recipes match your rules" @@ -44,5 +44,28 @@ "second": "second|seconds", "millisecond": "millisecond|milliseconds", "microsecond": "microsecond|microseconds" + }, + "emails": { + "password": { + "subject": "Mealie Forgot Password", + "header_text": "Forgot Password", + "message_top": "You have requested to reset your password.", + "message_bottom": "Please click the button above to reset your password.", + "button_text": "Reset Password" + }, + "invitation": { + "subject": "Invitation to join Mealie", + "header_text": "You're Invited!", + "message_top": "You have been invited to join Mealie.", + "message_bottom": "Please click the button above to accept the invitation.", + "button_text": "Accept Invitation" + }, + "test": { + "subject": "Mealie Test Email", + "header_text": "Test Email", + "message_top": "This is a test email.", + "message_bottom": "Please click the button above to test the email.", + "button_text": "Open Mealie" + } } } diff --git a/mealie/routes/admin/admin_email.py b/mealie/routes/admin/admin_email.py index aa3ea36d7c1..0c87676409a 100644 --- a/mealie/routes/admin/admin_email.py +++ b/mealie/routes/admin/admin_email.py @@ -1,4 +1,6 @@ -from fastapi import APIRouter +from typing import Annotated + +from fastapi import APIRouter, Header from mealie.routes._base import BaseAdminController, controller from mealie.schema.admin.email import EmailReady, EmailSuccess, EmailTest @@ -15,8 +17,12 @@ async def check_email_config(self): return EmailReady(ready=self.settings.SMTP_ENABLE) @router.post("", response_model=EmailSuccess) - async def send_test_email(self, data: EmailTest): - service = EmailService() + async def send_test_email( + self, + data: EmailTest, + accept_language: Annotated[str | None, Header()] = None, + ): + service = EmailService(locale=accept_language) status = False error = None diff --git a/mealie/routes/groups/controller_invitations.py b/mealie/routes/groups/controller_invitations.py index 24574b82e0a..cadd9dc29d3 100644 --- a/mealie/routes/groups/controller_invitations.py +++ b/mealie/routes/groups/controller_invitations.py @@ -1,4 +1,6 @@ -from fastapi import APIRouter, HTTPException, status +from typing import Annotated + +from fastapi import APIRouter, Header, HTTPException, status from mealie.core.security import url_safe_token from mealie.routes._base import BaseUserController, controller @@ -23,14 +25,21 @@ def get_invite_tokens(self): @router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED) def create_invite_token(self, uses: CreateInviteToken): if not self.user.can_invite: - raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not allowed to create invite tokens") + raise HTTPException( + status.HTTP_403_FORBIDDEN, + detail="User is not allowed to create invite tokens", + ) token = SaveInviteToken(uses_left=uses.uses, group_id=self.group_id, token=url_safe_token()) return self.repos.group_invite_tokens.create(token) @router.post("/email", response_model=EmailInitationResponse) - def email_invitation(self, invite: EmailInvitation): - email_service = EmailService() + def email_invitation( + self, + invite: EmailInvitation, + accept_language: Annotated[str | None, Header()] = None, + ): + email_service = EmailService(locale=accept_language) url = f"{self.settings.BASE_URL}/register?token={invite.token}" success = False diff --git a/mealie/routes/users/forgot_password.py b/mealie/routes/users/forgot_password.py index 096ee2d4232..f19904ff743 100644 --- a/mealie/routes/users/forgot_password.py +++ b/mealie/routes/users/forgot_password.py @@ -1,4 +1,6 @@ -from fastapi import APIRouter, Depends +from typing import Annotated + +from fastapi import APIRouter, Depends, Header from sqlalchemy.orm.session import Session from mealie.db.db_setup import generate_session @@ -9,10 +11,14 @@ @router.post("/forgot-password") -def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)): +def forgot_password( + email: ForgotPassword, + session: Session = Depends(generate_session), + accept_language: Annotated[str | None, Header()] = None, +): """Sends an email with a reset link to the user""" f_service = PasswordResetService(session) - return f_service.send_reset_email(email.email) + return f_service.send_reset_email(email.email, accept_language) @router.post("/reset-password") diff --git a/mealie/services/email/email_service.py b/mealie/services/email/email_service.py index 533d135b2aa..a66b3369cc6 100644 --- a/mealie/services/email/email_service.py +++ b/mealie/services/email/email_service.py @@ -4,6 +4,8 @@ from pydantic import BaseModel from mealie.core.root_logger import get_logger +from mealie.lang import local_provider +from mealie.lang.providers import Translator from mealie.services._base_service import BaseService from .email_senders import ABCEmailSender, DefaultEmailSender @@ -28,10 +30,11 @@ def render_html(self, template: Path) -> str: class EmailService(BaseService): - def __init__(self, sender: ABCEmailSender | None = None) -> None: + def __init__(self, sender: ABCEmailSender | None = None, locale: str | None = None) -> None: self.templates_dir = CWD / "templates" self.default_template = self.templates_dir / "default.html" self.sender: ABCEmailSender = sender or DefaultEmailSender() + self.translator: Translator = local_provider(locale) super().__init__() @@ -43,33 +46,33 @@ def send_email(self, email_to: str, data: EmailTemplate) -> bool: def send_forgot_password(self, address: str, reset_password_url: str) -> bool: forgot_password = EmailTemplate( - subject="Mealie Forgot Password", - header_text="Forgot Password", - message_top="You have requested to reset your password.", - message_bottom="Please click the button above to reset your password.", + subject=self.translator.t("emails.password.subject"), + header_text=self.translator.t("emails.password.header_text"), + message_top=self.translator.t("emails.password.message_top"), + message_bottom=self.translator.t("emails.password.message_bottom"), button_link=reset_password_url, - button_text="Reset Password", + button_text=self.translator.t("emails.password.button_text"), ) return self.send_email(address, forgot_password) def send_invitation(self, address: str, invitation_url: str) -> bool: invitation = EmailTemplate( - subject="Invitation to join Mealie", - header_text="You're Invited!", - message_top="You have been invited to join Mealie.", - message_bottom="Please click the button above to accept the invitation.", + subject=self.translator.t("emails.invitation.subject"), + header_text=self.translator.t("emails.invitation.header_text"), + message_top=self.translator.t("emails.invitation.message_top"), + message_bottom=self.translator.t("emails.invitation.message_bottom"), button_link=invitation_url, - button_text="Accept Invitation", + button_text=self.translator.t("emails.invitation.button_text"), ) return self.send_email(address, invitation) def send_test_email(self, address: str) -> bool: test_email = EmailTemplate( - subject="Test Email", - header_text="Test Email", - message_top="This is a test email.", - message_bottom="Please click the button above to test the email.", - button_link="https://www.google.com", - button_text="Test Email", + subject=self.translator.t("emails.test.subject"), + header_text=self.translator.t("emails.test.header_text"), + message_top=self.translator.t("emails.test.message_top"), + message_bottom=self.translator.t("emails.test.message_bottom"), + button_link=self.settings.BASE_URL, + button_text=self.translator.t("emails.test.button_text"), ) return self.send_email(address, test_email) diff --git a/mealie/services/user_services/password_reset_service.py b/mealie/services/user_services/password_reset_service.py index 4aa19647bd1..7aeb1de8dc0 100644 --- a/mealie/services/user_services/password_reset_service.py +++ b/mealie/services/user_services/password_reset_service.py @@ -32,14 +32,14 @@ def generate_reset_token(self, email: str) -> SavePasswordResetToken | None: return self.db.tokens_pw_reset.create(save_token) - def send_reset_email(self, email: str): + def send_reset_email(self, email: str, accept_language: str | None = None): token_entry = self.generate_reset_token(email) if token_entry is None: return None # Send Email - email_servive = EmailService() + email_servive = EmailService(locale=accept_language) reset_url = f"{self.settings.BASE_URL}/reset-password/?token={token_entry.token}" try: diff --git a/tests/unit_tests/services_tests/test_email_service.py b/tests/unit_tests/services_tests/test_email_service.py index 7c9c3844e36..6c90e5d4669 100644 --- a/tests/unit_tests/services_tests/test_email_service.py +++ b/tests/unit_tests/services_tests/test_email_service.py @@ -6,7 +6,7 @@ FAKE_ADDRESS = "my_secret_email@example.com" -SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Test Email"} +SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Mealie Test Email"} class TestEmailSender(ABCEmailSender):