diff --git a/backend/utils/email_sender.py b/backend/utils/email_sender.py index afaded2..bf7ecbb 100644 --- a/backend/utils/email_sender.py +++ b/backend/utils/email_sender.py @@ -1,62 +1,186 @@ +from collections import defaultdict from flask import current_app from flask_mail import Mail, Message import os + +# --------------------------------------------------------------------------- +# Shared email template +# --------------------------------------------------------------------------- + +def _build_email_html(content_html: str, frontend_url: str) -> str: + """Wrap content in the standard Chorly branded email shell.""" + logo_url = f"{frontend_url}/images/full_logo.png" + return f""" + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Chorly +
 
+ {content_html} +
+ Chorly — Making Chores Fun +
+
+ + +""" + + +def _primary_button(label: str, url: str) -> str: + return ( + f'' + f'{label}' + ) + + +def _action_button(label: str, url: str, bg: str, fg: str = '#ffffff') -> str: + return ( + f'' + f'{label}' + ) + + +# --------------------------------------------------------------------------- +# Email senders +# --------------------------------------------------------------------------- + def send_verification_email(to_email: str, token: str) -> None: - verify_url = f"{current_app.config['FRONTEND_URL']}/auth/verify?token={token}" - html_body = f'Click here to verify your account.' + frontend_url = current_app.config['FRONTEND_URL'] + verify_url = f"{frontend_url}/auth/verify?token={token}" + + content = f""" +

Verify your account

+

+ Welcome to Chorly! Click the button below to confirm your email address + and activate your account. +

+

+ {_primary_button('Verify My Account', verify_url)} +

+

+ If you didn't create a Chorly account, you can safely ignore this email.
+ Or copy this link into your browser: {verify_url} +

+ """ + html_body = _build_email_html(content, frontend_url) + msg = Message( - subject="Verify your account", + subject="Verify your Chorly account", recipients=[to_email], html=html_body, sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local') ) try: - if os.environ.get('DB_ENV') =='e2e': - return # Skip sending emails during e2e tests to avoid cluttering inboxes and logs + if os.environ.get('DB_ENV') == 'e2e': + return Mail(current_app).send(msg) print(f"[EMAIL to {to_email}] Verification: {verify_url}") except Exception: print(f"Failed to send email to {to_email}. Verification link: {verify_url}") + def send_reset_password_email(to_email: str, token: str) -> None: - reset_url = f"{current_app.config['FRONTEND_URL']}/auth/reset-password?token={token}" - html_body = f'Click here to reset your password.' + frontend_url = current_app.config['FRONTEND_URL'] + reset_url = f"{frontend_url}/auth/reset-password?token={token}" + + content = f""" +

Reset your password

+

+ We received a request to reset your Chorly password. Click the button below + to choose a new one. This link expires in 10 minutes. +

+

+ {_primary_button('Reset My Password', reset_url)} +

+

+ If you didn't request a password reset, you can safely ignore this email — + your password will not change.
+ Or copy this link into your browser: {reset_url} +

+ """ + html_body = _build_email_html(content, frontend_url) + msg = Message( - subject="Reset your password", + subject="Reset your Chorly password", recipients=[to_email], html=html_body, sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local') ) try: - if os.environ.get('DB_ENV') =='e2e': - return # Skip sending emails during e2e tests to avoid cluttering inboxes and logs + if os.environ.get('DB_ENV') == 'e2e': + return Mail(current_app).send(msg) print(f"[EMAIL to {to_email}] Reset password: {reset_url}") except Exception: print(f"Failed to send email to {to_email}. Reset link: {reset_url}") + def send_pin_setup_email(to_email: str, code: str) -> None: - html_body = f""" -
-

Set up your Parent PIN

-

To set your Parent PIN, enter the following code in the app:

-
{code}
-

This code is valid for 10 minutes.

-

If you did not request this, you can ignore this email.

-
-
Reward App
-
+ frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173') + + content = f""" +

Set up your Parent PIN

+

+ To set your Parent PIN, enter the following code in the app. + It is valid for 10 minutes. +

+
+ {code} +
+

+ If you did not request this, you can safely ignore this email. +

""" + html_body = _build_email_html(content, frontend_url) + msg = Message( - subject="Set up your Parent PIN", + subject="Set up your Chorly Parent PIN", recipients=[to_email], html=html_body, sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local') ) try: - if os.environ.get('DB_ENV') =='e2e': - return # Skip sending emails during e2e tests to avoid cluttering inboxes and logs + if os.environ.get('DB_ENV') == 'e2e': + return Mail(current_app).send(msg) print(f"[EMAIL to {to_email}] Parent PIN setup code: {code}") except Exception: @@ -74,8 +198,6 @@ def send_digest_email(to_email: str, items: list, unsubscribe_token: str) -> Non frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173') unsubscribe_url = f"{frontend_url}/api/digest-unsubscribe/{unsubscribe_token}" - # Group items by child - from collections import defaultdict by_child: dict = defaultdict(list) for item in items: by_child[item['child_name']].append(item) @@ -87,48 +209,46 @@ def send_digest_email(to_email: str, items: list, unsubscribe_token: str) -> Non entity_label = 'Chore' if item['entity_type'] == 'chore' else 'Reward' rows += f""" - - {item['entity_name']} - {entity_label} + + {item['entity_name']} + {entity_label} - - View - Approve - Deny + + {_action_button('View', item['view_url'], '#667eea')} + {_action_button('Approve', item['approve_url'], '#22c55e')} + {_action_button('Deny', item['deny_url'], '#ef4444')} """ child_sections += f""" -
-

{child_name}

- +
+
{child_name}
+
{rows}
""" - html_body = f""" -
-
-

Reward App — Daily Summary

-
-
-

- You have pending items that need your attention: -

- {child_sections} -
-
- Reward App • - Unsubscribe from daily digest -
-
+ content = f""" +

Daily Summary

+

+ You have pending items that need your attention: +

+ {child_sections} +

+ + Unsubscribe from daily digest + +

""" + html_body = _build_email_html(content, frontend_url) msg = Message( - subject='Reward App — Daily Summary', + subject='Chorly \u2014 Daily Summary', recipients=[to_email], html=html_body, sender=current_app.config.get('MAIL_DEFAULT_SENDER', 'no-reply@reward-app.local'),