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 @staticmethod async def send_feedback_email( to_email: str, reply_to_email: str, feedback_type: str, message: str, user_email: str, account_name: str | None = None, account_code: str | None = None, ) -> bool: if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend from datetime import datetime, timezone resend.api_key = settings.RESEND_API_KEY date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d") code_suffix = f" — {account_code}" if account_code else "" subject = f"[ResolutionFlow Feedback] {feedback_type} — {date_str}{code_suffix}" html = _render_feedback_html( feedback_type=feedback_type, message=message, user_email=user_email, account_name=account_name, account_code=account_code, ) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "reply_to": reply_to_email, "subject": subject, "html": html, } ) logger.info("Feedback email sent from %s (type: %s)", user_email, feedback_type) return True except Exception: logger.exception("Failed to send feedback email from %s", user_email) return False @staticmethod async def send_feedback_confirmation_email( to_email: str, feedback_type: str, message_preview: str, ) -> bool: """Send a thank-you confirmation to the feedback submitter. Fire-and-forget.""" if not settings.email_enabled: logger.warning("Confirmation email not sent — RESEND_API_KEY not configured") return False try: import resend resend.api_key = settings.RESEND_API_KEY subject = "Thanks for your feedback — ResolutionFlow" html = _render_feedback_confirmation_html( feedback_type=feedback_type, message_preview=message_preview, ) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Feedback confirmation email sent to %s", to_email) return True except Exception: logger.exception("Failed to send feedback confirmation 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.

""" def _render_feedback_html( feedback_type: str, message: str, user_email: str, account_name: str | None, account_code: str | None, ) -> str: from datetime import datetime, timezone import html date_str = datetime.now(timezone.utc).strftime("%B %d, %Y") safe_message = html.escape(message).replace("\n", "
") account_line = "" if account_name and account_code: account_line = f"""

Account: {html.escape(account_name)} ({html.escape(account_code)})

""" return f"""
{account_line}

ResolutionFlow Feedback

Type: {html.escape(feedback_type)}

From: {html.escape(user_email)}

Date: {date_str}

{safe_message}

Reply directly to this email to respond to the user.

""" def _render_feedback_confirmation_html( feedback_type: str, message_preview: str, ) -> str: import html safe_preview = html.escape(message_preview) return f"""

ResolutionFlow

Thanks for your feedback!

We've received your {html.escape(feedback_type)} and our team will review it shortly.

Your feedback

"{safe_preview}"

If we need more details, we'll reach out to you directly.

"""