""" Base class for sending emails to user after creating keys or invite links """ import json import os from typing import List, Optional from litellm_enterprise.types.enterprise_callbacks.send_emails import ( EmailEvent, EmailParams, SendKeyCreatedEmailEvent, ) from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.email_templates.email_footer import EMAIL_FOOTER from litellm.integrations.email_templates.key_created_email import ( KEY_CREATED_EMAIL_TEMPLATE, ) from litellm.integrations.email_templates.user_invitation_email import ( USER_INVITATION_EMAIL_TEMPLATE, ) from litellm.proxy._types import WebhookEvent from litellm.types.integrations.slack_alerting import LITELLM_LOGO_URL class BaseEmailLogger(CustomLogger): DEFAULT_LITELLM_EMAIL = "notifications@alerts.litellm.ai" DEFAULT_SUPPORT_EMAIL = "support@berri.ai" async def send_user_invitation_email(self, event: WebhookEvent): """ Send email to user after inviting them to the team """ email_params = await self._get_email_params( email_event=EmailEvent.new_user_invitation, user_id=event.user_id, user_email=getattr(event, "user_email", None), ) # Implement invitation email logic using email_params verbose_proxy_logger.debug( f"send_user_invitation_email_event: {json.dumps(event, indent=4, default=str)}" ) email_html_content = USER_INVITATION_EMAIL_TEMPLATE.format( email_logo_url=email_params.logo_url, recipient_email=email_params.recipient_email, base_url=email_params.base_url, email_support_contact=email_params.support_contact, email_footer=EMAIL_FOOTER, ) await self.send_email( from_email=self.DEFAULT_LITELLM_EMAIL, to_email=[email_params.recipient_email], subject=f"LiteLLM: {event.event_message}", html_body=email_html_content, ) pass async def send_key_created_email( self, send_key_created_email_event: SendKeyCreatedEmailEvent ): """ Send email to user after creating key for the user """ email_params = await self._get_email_params( user_id=send_key_created_email_event.user_id, user_email=send_key_created_email_event.user_email, email_event=EmailEvent.virtual_key_created, ) verbose_proxy_logger.debug( f"send_key_created_email_event: {json.dumps(send_key_created_email_event, indent=4, default=str)}" ) email_html_content = KEY_CREATED_EMAIL_TEMPLATE.format( email_logo_url=email_params.logo_url, recipient_email=email_params.recipient_email, key_budget=self._format_key_budget(send_key_created_email_event.max_budget), key_token=send_key_created_email_event.virtual_key, base_url=email_params.base_url, email_support_contact=email_params.support_contact, email_footer=EMAIL_FOOTER, ) await self.send_email( from_email=self.DEFAULT_LITELLM_EMAIL, to_email=[email_params.recipient_email], subject=f"LiteLLM: {send_key_created_email_event.event_message}", html_body=email_html_content, ) pass async def _get_email_params( self, email_event: EmailEvent, user_id: Optional[str] = None, user_email: Optional[str] = None, ) -> EmailParams: """ Get common email parameters used across different email sending methods Returns: EmailParams object containing logo_url, support_contact, base_url, and recipient_email """ logo_url = os.getenv("EMAIL_LOGO_URL", None) or LITELLM_LOGO_URL support_contact = os.getenv("EMAIL_SUPPORT_CONTACT", self.DEFAULT_SUPPORT_EMAIL) base_url = os.getenv("PROXY_BASE_URL", "http://0.0.0.0:4000") recipient_email: Optional[ str ] = user_email or await self._lookup_user_email_from_db(user_id=user_id) if recipient_email is None: raise ValueError( f"User email not found for user_id: {user_id}. User email is required to send email." ) # if user invited event then send invitation link if email_event == EmailEvent.new_user_invitation: base_url = await self._get_invitation_link( user_id=user_id, base_url=base_url ) return EmailParams( logo_url=logo_url, support_contact=support_contact, base_url=base_url, recipient_email=recipient_email, ) def _format_key_budget(self, max_budget: Optional[float]) -> str: """ Format the key budget to be displayed in the email """ if max_budget is None: return "No budget" return f"${max_budget}" async def _lookup_user_email_from_db(self, user_id: Optional[str]) -> Optional[str]: """ Lookup user email from user_id """ from litellm.proxy.proxy_server import prisma_client if prisma_client is None: verbose_proxy_logger.debug( f"Prisma client not found. Unable to lookup user email for user_id: {user_id}" ) return None user_row = await prisma_client.db.litellm_usertable.find_unique( where={"user_id": user_id} ) if user_row is not None: return user_row.user_email return None async def _get_invitation_link(self, user_id: Optional[str], base_url: str) -> str: """ Get invitation link for the user """ import asyncio from litellm.proxy.proxy_server import prisma_client ################################################################################ ########## Sleep for 10 seconds to wait for the invitation link to be created ### ################################################################################ # The UI, calls /invitation/new to generate the invitation link # We wait 10 seconds to ensure the link is created ################################################################################ await asyncio.sleep(10) if prisma_client is None: verbose_proxy_logger.debug( f"Prisma client not found. Unable to lookup user email for user_id: {user_id}" ) return base_url if user_id is None: return base_url # get the latest invitation link for the user invitation_rows = await prisma_client.db.litellm_invitationlink.find_many( where={"user_id": user_id}, order={"created_at": "desc"}, ) if len(invitation_rows) > 0: invitation_row = invitation_rows[0] return self._construct_invitation_link( invitation_id=invitation_row.id, base_url=base_url ) return base_url def _construct_invitation_link(self, invitation_id: str, base_url: str) -> str: """ Construct invitation link for the user # http://localhost:4000/ui?invitation_id=7a096b3a-37c6-440f-9dd1-ba22e8043f6b """ return f"{base_url}/ui?invitation_id={invitation_id}" async def send_email( self, from_email: str, to_email: List[str], subject: str, html_body: str, ): pass