import logging from typing import TYPE_CHECKING from app.core.config import settings if TYPE_CHECKING: from app.models.sales_lead import SalesLead 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_email_verification_email( to_email: str, verification_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 = "Verify Your Email — ResolutionFlow" html = _render_email_verification_html(verification_url=verification_url) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Verification email sent to %s", to_email) return True except Exception: logger.exception("Failed to send verification 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 @staticmethod async def send_survey_notification_email( to_email: str, respondent_name: str | None, responses: dict, ) -> bool: """Send survey response notification. Fire-and-forget.""" if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend import html as html_mod from datetime import datetime, timezone resend.api_key = settings.RESEND_API_KEY name_display = html_mod.escape(respondent_name) if respondent_name else "Anonymous" date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") subject = f"[FlowPilot Survey] New Response from {name_display} — {date_str}" rows_html = "" for q_id, answer in responses.items(): safe_id = html_mod.escape(str(q_id)) if isinstance(answer, list): answer_display = "
".join(f"• {html_mod.escape(str(item))}" for item in answer) else: answer_display = html_mod.escape(str(answer)) rows_html += f""" {safe_id} {answer_display} """ email_html = f"""

FlowPilot Survey Response

From: {name_display} • {date_str}

{rows_html}

This response has been saved to the survey_responses table.

""" resend.Emails.send({ "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": email_html, }) logger.info("Survey notification sent for respondent: %s", name_display) return True except Exception: logger.exception("Failed to send survey notification email") return False @staticmethod async def send_survey_copy_email( to_email: str, respondent_name: str | None, formatted_responses: str, ) -> bool: """Send a copy of survey responses to the respondent.""" if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend import html as html_mod resend.api_key = settings.RESEND_API_KEY safe_name = html_mod.escape(respondent_name or "there") safe_responses = html_mod.escape(formatted_responses).replace("\n", "
") subject = "Your FlowPilot Survey Responses" email_html = f"""

ResolutionFlow

Your Survey Responses

Hi {safe_name}, here's a copy of your FlowPilot survey responses for your records.

{safe_responses}

Thank you for your contribution to FlowPilot research.

""" resend.Emails.send({ "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": email_html, }) logger.info("Survey copy email sent to %s", to_email) return True except Exception: logger.exception("Failed to send survey copy email to %s", to_email) return False @staticmethod async def send_beta_signup_notification( signup_email: str, notify_email: str = "beta@resolutionflow.com", ) -> bool: """Notify beta@resolutionflow.com about a new beta signup. Fire-and-forget.""" if not settings.email_enabled: logger.warning("Beta signup email not sent — RESEND_API_KEY not configured") return False try: import resend import html as html_mod from datetime import datetime, timezone resend.api_key = settings.RESEND_API_KEY date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") safe_email = html_mod.escape(signup_email) subject = f"[ResolutionFlow Beta] New signup — {safe_email}" email_html = f"""

ResolutionFlow

New Beta Signup

A new user has requested beta access:

Email

{safe_email}

Submitted at {date_str}

""" resend.Emails.send({ "from": settings.FROM_EMAIL, "to": [notify_email], "reply_to": signup_email, "subject": subject, "html": email_html, }) logger.info("Beta signup notification sent for %s", signup_email) return True except Exception: logger.exception("Failed to send beta signup notification for %s", signup_email) return False @staticmethod async def send_sales_lead_notification( to_email: str, lead: "SalesLead", ) -> bool: """Notify the sales recipient about a new Talk-to-Sales submission. Fire-and-forget. Returns False (and logs) on any failure; never raises. """ if not settings.email_enabled: logger.warning( "Sales lead email not sent — RESEND_API_KEY not configured (lead %s)", lead.id, ) return False try: import resend import html as html_mod from datetime import datetime, timezone resend.api_key = settings.RESEND_API_KEY date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") safe_email = html_mod.escape(lead.email) safe_name = html_mod.escape(lead.name) safe_company = html_mod.escape(lead.company) safe_team_size = html_mod.escape(lead.team_size or "—") safe_source = html_mod.escape(lead.source) safe_message = html_mod.escape(lead.message or "(no message)") subject = f"[ResolutionFlow Sales] New lead — {safe_company} ({safe_email})" email_html = f"""

ResolutionFlow

New Sales Lead

Source: {safe_source}

Name

{safe_name}

Email

{safe_email}

Company

{safe_company}

Team Size

{safe_team_size}

Message

{safe_message}

Submitted at {date_str} · Lead ID: {lead.id}

""" resend.Emails.send({ "from": settings.FROM_EMAIL, "to": [to_email], "reply_to": lead.email, "subject": subject, "html": email_html, }) logger.info("Sales lead notification sent for %s (lead %s)", lead.email, lead.id) return True except Exception: logger.exception( "Failed to send sales lead notification for %s (lead %s)", lead.email, lead.id, ) return False @staticmethod async def send_notification_email( to_email: str, title: str, body: str, link_url: str | None = None, ) -> bool: """Send a notification email. Fire-and-forget.""" 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 = f"[ResolutionFlow] {title}" html = _render_notification_html( title=title, body=body, link_url=link_url, ) resend.Emails.send( { "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": html, } ) logger.info("Notification email sent to %s: %s", to_email, title) return True except Exception: logger.exception("Failed to send notification email to %s", to_email) return False @staticmethod async def send_survey_invite_email( to_email: str, recipient_name: str, survey_url: str, ) -> bool: """Send survey invite email.""" if not settings.email_enabled: logger.warning("Email not sent — RESEND_API_KEY not configured") return False try: import resend import html as html_mod resend.api_key = settings.RESEND_API_KEY safe_name = html_mod.escape(recipient_name) subject = "FlowPilot Survey — Your expertise matters" email_html = f"""

ResolutionFlow

FlowPilot Research

Hi {safe_name},

We're building an AI assistant for MSP engineers and would love your input. This 5-minute survey will help shape how FlowPilot thinks about troubleshooting.

Take the Survey

Your responses are confidential. Takes about 5 minutes.

""" resend.Emails.send({ "from": settings.FROM_EMAIL, "to": [to_email], "subject": subject, "html": email_html, }) logger.info("Survey invite email sent to %s", to_email) return True except Exception: logger.exception("Failed to send survey 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.

""" 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_email_verification_html(verification_url: str) -> str: return f"""

ResolutionFlow

Decision Tree Platform for MSP Professionals

Please verify your email address by clicking the button below. This link expires in 24 hours.

Verify Email

If you didn't create an account, you can safely ignore this email.

""" 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.

""" def _render_notification_html( title: str, body: str, link_url: str | None = None, ) -> str: import html as html_mod safe_title = html_mod.escape(title) safe_body = html_mod.escape(body) link_section = "" if link_url: link_section = f""" View in ResolutionFlow """ return f"""
{link_section}

ResolutionFlow

{safe_title}

{safe_body}

— ResolutionFlow

"""