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: 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 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 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: 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 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 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: 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 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 Mail(current_app).send(msg) print(f"[EMAIL to {to_email}] Parent PIN setup code: {code}") except Exception: print(f"Failed to send email to {to_email}. Parent PIN setup code: {code}") def send_digest_email(to_email: str, items: list, unsubscribe_token: str) -> None: """ Send the nightly digest email listing pending chore/reward items. Each item in `items` is a dict with keys: child_name, entity_name, entity_type, view_url, approve_url, deny_url Items are grouped by child_name in the email. """ frontend_url = current_app.config.get('FRONTEND_URL', 'https://localhost:5173') unsubscribe_url = f"{frontend_url}/api/digest-unsubscribe/{unsubscribe_token}" by_child: dict = defaultdict(list) for item in items: by_child[item['child_name']].append(item) child_sections = '' for child_name, child_items in by_child.items(): rows = '' for item in child_items: entity_label = 'Chore' if item['entity_type'] == 'chore' else 'Reward' rows += f""" {item['entity_name']} {entity_label} {_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}
{rows}
""" 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='Chorly \u2014 Daily Summary', 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 Mail(current_app).send(msg) print(f"[EMAIL to {to_email}] Daily digest sent ({len(items)} items)") except Exception: print(f"Failed to send digest email to {to_email}")