From 967fed1fa1e03ae476be83d7c3f484973151dbbf Mon Sep 17 00:00:00 2001 From: Shivam Rawat Date: Sat, 30 May 2026 10:09:05 -0700 Subject: [PATCH] feat(enterprise): add RESEND_FROM_EMAIL for self-hosted Resend sends (#28830) Allow self-hosted installs to override the default LiteLLM sender address via RESEND_FROM_EMAIL, matching SendGrid's SENDGRID_SENDER_EMAIL pattern. Co-authored-by: Cursor --- .../send_emails/resend_email.py | 19 ++++- .../send_emails/test_resend_email.py | 76 +++++++++++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py index 7593e66aa4..3fad5601f5 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py @@ -19,12 +19,26 @@ RESEND_API_ENDPOINT = "https://api.resend.com/emails" class ResendEmailLogger(BaseEmailLogger): + """ + Send emails using Resend's API. + + Required env vars: + - RESEND_API_KEY + + Optional env vars: + - RESEND_FROM_EMAIL: Override the default sender address. Must be on a + domain verified in your Resend account. When unset, falls back to the + `from_email` argument passed by the caller (which defaults to + `notifications@alerts.litellm.ai` and only works on LiteLLM Cloud). + """ + def __init__(self, internal_usage_cache=None, **kwargs): super().__init__(internal_usage_cache=internal_usage_cache, **kwargs) self.async_httpx_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.LoggingCallback ) self.resend_api_key = os.getenv("RESEND_API_KEY") + self.resend_from_email = os.getenv("RESEND_FROM_EMAIL") async def send_email( self, @@ -33,13 +47,14 @@ class ResendEmailLogger(BaseEmailLogger): subject: str, html_body: str, ): + sender_email = self.resend_from_email or from_email verbose_logger.debug( - f"Sending email from {from_email} to {to_email} with subject {subject}" + f"Sending email from {sender_email} to {to_email} with subject {subject}" ) response = await self.async_httpx_client.post( url=RESEND_API_ENDPOINT, json={ - "from": from_email, + "from": sender_email, "to": to_email, "subject": subject, "html": html_body, diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py index b07216921e..88cc2275ae 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py @@ -32,7 +32,11 @@ def clear_client_cache(): @pytest.fixture def mock_env_vars(): - with mock.patch.dict(os.environ, {"RESEND_API_KEY": "test_api_key"}): + # Set test API key and ensure RESEND_FROM_EMAIL is unset for isolation + # so tests can verify the default `from_email` argument is used. + patched = {"RESEND_API_KEY": "test_api_key"} + with mock.patch.dict(os.environ, patched): + os.environ.pop("RESEND_FROM_EMAIL", None) yield @@ -87,7 +91,7 @@ async def test_send_email_success(mock_env_vars): async def test_send_email_missing_api_key(): # Remove the API key from environment before initializing logger original_key = os.environ.pop("RESEND_API_KEY", None) - + try: # Initialize the logger after removing the API key logger = ResendEmailLogger() @@ -104,16 +108,19 @@ async def test_send_email_missing_api_key(): mock_response.raise_for_status.return_value = None mock_response.status_code = 200 mock_response.json.return_value = {"id": "test_email_id"} - + mock_async_client = mock.AsyncMock() mock_async_client.post.return_value = mock_response - + # Directly inject the mock client to bypass any caching logger.async_httpx_client = mock_async_client # Send email await logger.send_email( - from_email=from_email, to_email=to_email, subject=subject, html_body=html_body + from_email=from_email, + to_email=to_email, + subject=subject, + html_body=html_body, ) # Verify the HTTP client was called with None as the API key @@ -159,3 +166,62 @@ async def test_send_email_multiple_recipients(mock_env_vars): call_args = mock_async_client.post.call_args request_body = call_args[1]["json"] assert request_body["to"] == to_email + + +@pytest.mark.asyncio +async def test_send_email_uses_resend_from_email_override(): + """RESEND_FROM_EMAIL overrides the caller-supplied from_email.""" + with mock.patch.dict( + os.environ, + { + "RESEND_API_KEY": "test_api_key", + "RESEND_FROM_EMAIL": "alerts@my-verified-domain.com", + }, + ): + logger = ResendEmailLogger() + + mock_response = mock.Mock(spec=Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test_email_id"} + mock_response.raise_for_status.return_value = None + + mock_async_client = mock.AsyncMock() + mock_async_client.post.return_value = mock_response + logger.async_httpx_client = mock_async_client + + await logger.send_email( + from_email="notifications@alerts.litellm.ai", + to_email=["recipient@example.com"], + subject="Test Subject", + html_body="

Test email body

", + ) + + mock_async_client.post.assert_called_once() + request_body = mock_async_client.post.call_args[1]["json"] + assert request_body["from"] == "alerts@my-verified-domain.com" + + +@pytest.mark.asyncio +async def test_send_email_falls_back_to_argument_when_override_unset(mock_env_vars): + """When RESEND_FROM_EMAIL is unset, the caller-supplied from_email is used.""" + logger = ResendEmailLogger() + + mock_response = mock.Mock(spec=Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test_email_id"} + mock_response.raise_for_status.return_value = None + + mock_async_client = mock.AsyncMock() + mock_async_client.post.return_value = mock_response + logger.async_httpx_client = mock_async_client + + await logger.send_email( + from_email="notifications@alerts.litellm.ai", + to_email=["recipient@example.com"], + subject="Test Subject", + html_body="

Test email body

", + ) + + mock_async_client.post.assert_called_once() + request_body = mock_async_client.post.call_args[1]["json"] + assert request_body["from"] == "notifications@alerts.litellm.ai"