From 07cb100fdc136436646ef19e1476badd3592643b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 10 May 2025 17:23:59 -0700 Subject: [PATCH] Litellm emails smtp fixes (#10730) * feat: smtp email logger * feat: smtp email logger * fix: linting errors --- .../send_emails/smtp_email.py | 47 +++++++++++++++++++ litellm/__init__.py | 1 + litellm/litellm_core_utils/litellm_logging.py | 16 ++++++- litellm/proxy/proxy_config.yaml | 2 +- litellm/proxy/utils.py | 32 +++++++++++-- 5 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py new file mode 100644 index 00000000000..4ede8ee59fe --- /dev/null +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py @@ -0,0 +1,47 @@ +""" +This is the litellm SMTP email integration +""" +import asyncio +from typing import List + +from litellm._logging import verbose_logger + +from .base_email import BaseEmailLogger + + +class SMTPEmailLogger(BaseEmailLogger): + """ + This is the litellm SMTP email integration + + Required SMTP environment variables: + - SMTP_HOST + - SMTP_PORT + - SMTP_USERNAME + - SMTP_PASSWORD + - SMTP_SENDER_EMAIL + """ + + def __init__(self): + verbose_logger.debug("SMTP Email Logger initialized....") + + async def send_email( + self, + from_email: str, + to_email: List[str], + subject: str, + html_body: str, + ): + from litellm.proxy.utils import send_email as send_smtp_email + + verbose_logger.debug( + f"Sending email from {from_email} to {to_email} with subject {subject}" + ) + for receiver_email in to_email: + asyncio.create_task( + send_smtp_email( + receiver_email=receiver_email, + subject=subject, + html=html_body, + ) + ) + return diff --git a/litellm/__init__.py b/litellm/__init__.py index eb712ac870f..241f2f712d9 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -117,6 +117,7 @@ _custom_logger_compatible_callbacks_literal = Literal[ "bedrock_vector_store", "generic_api", "resend_email", + "smtp_email", ] logged_real_time_event_types: Optional[Union[List[str], Literal["*"]]] = None _known_custom_logger_compatible_callbacks: List = list( diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 0789d7669d3..242f0873c83 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -140,13 +140,16 @@ try: from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import ( ResendEmailLogger, ) + from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import ( + SMTPEmailLogger, + ) except Exception as e: verbose_logger.debug( f"[Non-Blocking] Unable to import GenericAPILogger - LiteLLM Enterprise Feature - {str(e)}" ) GenericAPILogger = CustomLogger # type: ignore ResendEmailLogger = CustomLogger # type: ignore - + SMTPEmailLogger = CustomLogger # type: ignore _in_memory_loggers: List[Any] = [] ### GLOBAL VARIABLES ### @@ -3053,6 +3056,13 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 resend_email_logger = ResendEmailLogger() _in_memory_loggers.append(resend_email_logger) return resend_email_logger # type: ignore + elif logging_integration == "smtp_email": + for callback in _in_memory_loggers: + if isinstance(callback, SMTPEmailLogger): + return callback + smtp_email_logger = SMTPEmailLogger() + _in_memory_loggers.append(smtp_email_logger) + return smtp_email_logger # type: ignore elif logging_integration == "humanloop": for callback in _in_memory_loggers: if isinstance(callback, HumanloopLogger): @@ -3204,6 +3214,10 @@ def get_custom_logger_compatible_class( # noqa: PLR0915 for callback in _in_memory_loggers: if isinstance(callback, ResendEmailLogger): return callback + elif logging_integration == "smtp_email": + for callback in _in_memory_loggers: + if isinstance(callback, SMTPEmailLogger): + return callback return None except Exception as e: diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index cc7f15b8f65..db3baeb2b20 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -6,7 +6,7 @@ model_list: litellm_settings: - callbacks: ["resend_email"] + callbacks: ["smtp_email"] guardrails: - guardrail_name: "bedrock-pre-guard" diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 6e8c65710d6..6c551c0b4e8 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -2362,7 +2362,11 @@ async def _cache_user_row(user_id: str, cache: DualCache, db: PrismaClient): return -async def send_email(receiver_email, subject, html): +async def send_email( + receiver_email: Optional[str] = None, + subject: Optional[str] = None, + html: Optional[str] = None, +): """ smtp_host, smtp_port, @@ -2380,6 +2384,12 @@ async def send_email(receiver_email, subject, html): sender_email = os.getenv("SMTP_SENDER_EMAIL", None) if sender_email is None: raise ValueError("Trying to use SMTP, but SMTP_SENDER_EMAIL is not set") + if receiver_email is None: + raise ValueError(f"No receiver email provided for SMTP email. {receiver_email}") + if subject is None: + raise ValueError(f"No subject provided for SMTP email. {subject}") + if html is None: + raise ValueError(f"No HTML body provided for SMTP email. {html}") ## EMAIL SETUP ## email_message = MIMEMultipart() @@ -2398,19 +2408,31 @@ async def send_email(receiver_email, subject, html): try: # Establish a secure connection with the SMTP server - with smtplib.SMTP(smtp_host, smtp_port) as server: # type: ignore + with smtplib.SMTP( + host=smtp_host, + port=smtp_port, + ) as server: if os.getenv("SMTP_TLS", "True") != "False": server.starttls() # Login to your email account only if smtp_username and smtp_password are provided if smtp_username and smtp_password: - server.login(smtp_username, smtp_password) # type: ignore + server.login( + user=smtp_username, + password=smtp_password, + ) # Send the email - server.send_message(email_message) + server.send_message( + msg=email_message, + from_addr=sender_email, + to_addrs=receiver_email, + ) except Exception as e: - print_verbose("An error occurred while sending the email:" + str(e)) + verbose_proxy_logger.exception( + "An error occurred while sending the email:" + str(e) + ) def hash_token(token: str):