from datetime import datetime from jinja2 import Template from ..core.config import settings from ..core.logging import get_logger logger = get_logger(__name__) class EmailService: """Sends email via Mailgun REST API (httpx, async-safe).""" @property def _configured(self) -> bool: return bool(settings.mailgun_api_key and settings.mailgun_domain) async def _send(self, to_email: str, subject: str, html: str) -> bool: if not self._configured: logger.warning("Mailgun not configured — email skipped") return False try: import httpx url = f"https://api.mailgun.net/v3/{settings.mailgun_domain}/messages" async with httpx.AsyncClient(timeout=15) as client: response = await client.post( url, auth=("api", settings.mailgun_api_key), data={ "from": f"Accessible Video Platform <{settings.mailgun_from}>", "to": to_email, "subject": subject, "html": html, }, ) if response.status_code in (200, 202): logger.info(f"Email sent to {to_email}: {subject}") return True logger.error(f"Mailgun error {response.status_code}: {response.text}") return False except Exception as e: logger.error(f"Email sending failed: {e}") return False async def send_invitation_email( self, to_email: str, inviter_name: str, org_name: str, accept_url: str, expires_at: datetime, ) -> bool: html = Template("""

You've been invited!

Hi there,

{{ inviter_name }} has invited you to join {{ org_name }} on the Accessible Video Platform.

Accept Invitation

Or copy this link: {{ accept_url }}

This invitation expires on {{ expires_at }}.

""").render( inviter_name=inviter_name, org_name=org_name, accept_url=accept_url, expires_at=expires_at.strftime("%B %d, %Y"), ) return await self._send(to_email, f"You're invited to join {org_name}", html) async def send_welcome_email( self, to_email: str, full_name: str, org_name: str, ) -> bool: html = Template("""

Welcome to {{ org_name }}!

Hi {{ full_name }},

Your account has been set up and you now have access to {{ org_name }} on the Accessible Video Platform.

If you have any questions, reach out to your organization admin.

""").render(full_name=full_name, org_name=org_name) return await self._send(to_email, f"Welcome to {org_name}", html) async def send_password_reset_email( self, to_email: str, full_name: str, reset_url: str, ) -> bool: html = Template("""

Reset your password

Hi {{ full_name }},

Click below to reset your password. This link is valid for 1 hour.

Reset Password

If you didn't request this, you can ignore this email.

""").render(full_name=full_name, reset_url=reset_url) return await self._send(to_email, "Reset your password", html) async def send_completion_email( self, recipient_email: str, job_title: str, download_links: dict[str, dict[str, str]] ) -> bool: html = self._render_completion_template(job_title=job_title, download_links=download_links) return await self._send(recipient_email, f"Your accessible video assets are ready: {job_title}", html) def _render_completion_template( self, job_title: str, download_links: dict[str, dict[str, str]] ) -> str: """Render the completion email HTML template""" template_str = """ Your Accessible Video Assets Are Ready

Your Accessible Video Assets Are Ready!

{{ job_title }}

Great news! Your video accessibility assets have been processed and are ready for download.

{% for language, files in download_links.items() %}

{{ language.upper() }} Assets

{% for file_type, url in files.items() %} Download {{ file_type|replace('_', ' ')|title }} {% endfor %}
{% endfor %}

Important: These download links will expire in 24 hours for security purposes.

If you need assistance or have questions about your accessible video assets, please don't hesitate to contact our support team.

""" template = Template(template_str) return template.render( job_title=job_title, download_links=download_links ) # Global service instance email_service = EmailService()