diff --git a/enterprise/dist/litellm_enterprise-0.1.24-py3-none-any.whl b/enterprise/dist/litellm_enterprise-0.1.24-py3-none-any.whl new file mode 100644 index 0000000000..a26b0458c9 Binary files /dev/null and b/enterprise/dist/litellm_enterprise-0.1.24-py3-none-any.whl differ diff --git a/enterprise/dist/litellm_enterprise-0.1.24.tar.gz b/enterprise/dist/litellm_enterprise-0.1.24.tar.gz new file mode 100644 index 0000000000..4361910f4b Binary files /dev/null and b/enterprise/dist/litellm_enterprise-0.1.24.tar.gz differ diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py new file mode 100644 index 0000000000..33e00acd1b --- /dev/null +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py @@ -0,0 +1,79 @@ +""" +LiteLLM x SendGrid email integration. + +Docs: https://docs.sendgrid.com/api-reference/mail-send/mail-send +""" + +import os +from typing import List + +from litellm._logging import verbose_logger +from litellm.llms.custom_httpx.http_handler import ( + get_async_httpx_client, + httpxSpecialProvider, +) + +from .base_email import BaseEmailLogger + + +SENDGRID_API_ENDPOINT = "https://api.sendgrid.com/v3/mail/send" + + +class SendGridEmailLogger(BaseEmailLogger): + """ + Send emails using SendGrid's Mail Send API. + + Required env vars: + - SENDGRID_API_KEY + """ + + def __init__(self): + self.async_httpx_client = get_async_httpx_client( + llm_provider=httpxSpecialProvider.LoggingCallback + ) + self.sendgrid_api_key = os.getenv("SENDGRID_API_KEY") + verbose_logger.debug("SendGrid Email Logger initialized.") + + async def send_email( + self, + from_email: str, + to_email: List[str], + subject: str, + html_body: str, + ): + """ + Send an email via SendGrid. + """ + if not self.sendgrid_api_key: + raise ValueError("SENDGRID_API_KEY is not set") + + verbose_logger.debug( + f"Sending email via SendGrid from {from_email} to {to_email} with subject {subject}" + ) + + payload = { + "from": {"email": from_email}, + "personalizations": [ + { + "to": [{"email": email} for email in to_email], + "subject": subject, + } + ], + "content": [ + { + "type": "text/html", + "value": html_body, + } + ], + } + + response = await self.async_httpx_client.post( + url=SENDGRID_API_ENDPOINT, + json=payload, + headers={"Authorization": f"Bearer {self.sendgrid_api_key}"}, + ) + + verbose_logger.debug( + f"SendGrid response status={response.status_code}, body={response.text}" + ) + return diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py new file mode 100644 index 0000000000..4ecb4872aa --- /dev/null +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py @@ -0,0 +1,99 @@ +import os +import sys +import unittest.mock as mock + +import pytest +from httpx import Response + +sys.path.insert(0, os.path.abspath("../../..")) + +from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import ( + SendGridEmailLogger, +) + + +@pytest.fixture +def mock_env_vars(): + with mock.patch.dict(os.environ, {"SENDGRID_API_KEY": "test_api_key"}): + yield + + +@pytest.fixture +def mock_httpx_client(): + with mock.patch( + "litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email.get_async_httpx_client" + ) as mock_client: + mock_response = mock.AsyncMock(spec=Response) + mock_response.status_code = 202 + mock_response.text = "accepted" + + mock_async_client = mock.AsyncMock() + mock_async_client.post.return_value = mock_response + mock_client.return_value = mock_async_client + + yield mock_async_client + + +@pytest.mark.asyncio +async def test_send_email_success(mock_env_vars, mock_httpx_client): + logger = SendGridEmailLogger() + + from_email = "test@example.com" + to_email = ["recipient@example.com"] + subject = "Test Subject" + html_body = "

Test email body

" + + await logger.send_email( + from_email=from_email, to_email=to_email, subject=subject, html_body=html_body + ) + + mock_httpx_client.post.assert_called_once() + call_args = mock_httpx_client.post.call_args + assert call_args[1]["url"] == "https://api.sendgrid.com/v3/mail/send" + + payload = call_args[1]["json"] + assert payload["from"] == {"email": from_email} + assert payload["personalizations"][0]["to"] == [{"email": to_email[0]}] + assert payload["personalizations"][0]["subject"] == subject + assert payload["content"][0]["type"] == "text/html" + assert payload["content"][0]["value"] == html_body + + assert call_args[1]["headers"] == {"Authorization": "Bearer test_api_key"} + + +@pytest.mark.asyncio +async def test_send_email_missing_api_key(mock_httpx_client): + with mock.patch.dict(os.environ, {}, clear=True): + logger = SendGridEmailLogger() + + with pytest.raises(ValueError): + await logger.send_email( + from_email="test@example.com", + to_email=["recipient@example.com"], + subject="Test Subject", + html_body="

Test email body

", + ) + + mock_httpx_client.post.assert_not_called() + + +@pytest.mark.asyncio +async def test_send_email_multiple_recipients(mock_env_vars, mock_httpx_client): + logger = SendGridEmailLogger() + + from_email = "test@example.com" + to_email = ["recipient1@example.com", "recipient2@example.com"] + subject = "Test Subject" + html_body = "

Test email body

" + + await logger.send_email( + from_email=from_email, to_email=to_email, subject=subject, html_body=html_body + ) + + mock_httpx_client.post.assert_called_once() + payload = mock_httpx_client.post.call_args[1]["json"] + + assert payload["personalizations"][0]["to"] == [ + {"email": "recipient1@example.com"}, + {"email": "recipient2@example.com"}, + ]