import logging from app.core.config import settings logger = logging.getLogger(__name__) class EmailService: """Best-effort email delivery via Resend. Never raises on failure.""" @staticmethod async def send_invite_email( to_email: str, code: str, plan: str, trial_days: int | None = None, signup_url: str = "https://resolutionflow.com/register", ) -> bool: if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend resend.api_key = settings.RESEND_API_KEY plan_label = plan.capitalize() trial_text = f" with a {trial_days}-day free trial" if trial_days else "" subject = f"You're invited to ResolutionFlow ({plan_label} plan{trial_text})" html = _render_invite_html( code=code, plan_label=plan_label, trial_days=trial_days, signup_url=signup_url, ) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Invite email sent to %s", to_email) return True except Exception: logger.exception("Failed to send invite email to %s", to_email) return False @staticmethod async def send_password_reset_email( to_email: str, reset_url: str, ) -> bool: if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend resend.api_key = settings.RESEND_API_KEY subject = "Reset Your Password — ResolutionFlow" html = _render_password_reset_html(reset_url=reset_url) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Password reset email sent to %s", to_email) return True except Exception: logger.exception("Failed to send password reset email to %s", to_email) return False @staticmethod async def send_welcome_email( to_email: str, temp_password: str, login_url: str = "https://resolutionflow.com/login", ) -> bool: if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend resend.api_key = settings.RESEND_API_KEY subject = "Welcome to ResolutionFlow — Your Account Is Ready" html = _render_welcome_html( temp_password=temp_password, login_url=login_url, ) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Welcome email sent to %s", to_email) return True except Exception: logger.exception("Failed to send welcome email to %s", to_email) return False @staticmethod async def send_account_invite_email( to_email: str, code: str, account_name: str, role: str, signup_url: str = "https://resolutionflow.com/register", ) -> bool: if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend resend.api_key = settings.RESEND_API_KEY role_label = role.capitalize() subject = f"You've been invited to join {account_name} on ResolutionFlow" html = _render_account_invite_html( code=code, account_name=account_name, role_label=role_label, signup_url=signup_url, ) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Account invite email sent to %s", to_email) return True except Exception: logger.exception("Failed to send account invite email to %s", to_email) return False def _render_invite_html( code: str, plan_label: str, trial_days: int | None, signup_url: str, ) -> str: trial_section = "" if trial_days: trial_section = f"""

Your {trial_days}-day free trial starts when you register. After your trial ends, your account will revert to the Free plan.

""" return f"""
{trial_section}

ResolutionFlow

Decision Tree Platform for MSP Professionals

You've been invited to join ResolutionFlow on the {plan_label} plan.

Your Invite Code

{code}

Create Your Account

Enter the code above during registration, or click the button to get started.

""" def _render_account_invite_html( code: str, account_name: str, role_label: str, signup_url: str, ) -> str: return f"""

ResolutionFlow

Decision Tree Platform for MSP Professionals

You've been invited to join {account_name} as an {role_label}.

Your Invite Code

{code}

Create Your Account

Enter the code above during registration, or click the button to get started.

""" def _render_welcome_html( temp_password: str, login_url: str, ) -> str: return f"""

ResolutionFlow

Decision Tree Platform for MSP Professionals

Your account has been created. Use the temporary password below to sign in. You will be asked to change it on first login.

Temporary Password

{temp_password}

Sign In

For security, please change your password immediately after signing in.

""" def _render_password_reset_html(reset_url: str) -> str: return f"""

ResolutionFlow

Decision Tree Platform for MSP Professionals

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

Reset Your Password

If you didn't request this, you can safely ignore this email. Your password will not be changed.

"""