diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py index 8fc2d66d531..9dfa794efa5 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py @@ -5,7 +5,7 @@ Docs: https://docs.sendgrid.com/api-reference/mail-send/mail-send """ import os -from typing import List +from typing import Final, List from litellm._logging import verbose_logger from litellm.llms.custom_httpx.http_handler import ( @@ -25,6 +25,12 @@ class SendGridEmailLogger(BaseEmailLogger): Required env vars: - SENDGRID_API_KEY + + Optional env vars: + - SENDGRID_SENDER_EMAIL: Override the sender address. When unset, falls back to + the `from_email` argument passed by the caller. + - SENDGRID_REPLY_TO_EMAIL: Address recipients reply to. When unset, SendGrid + defaults Reply-To to the sender address. """ def __init__(self, internal_usage_cache=None, **kwargs): @@ -34,6 +40,7 @@ class SendGridEmailLogger(BaseEmailLogger): ) self.sendgrid_api_key = os.getenv("SENDGRID_API_KEY") self.sendgrid_sender_email = os.getenv("SENDGRID_SENDER_EMAIL") + self.sendgrid_reply_to_email = os.getenv("SENDGRID_REPLY_TO_EMAIL") verbose_logger.debug("SendGrid Email Logger initialized.") async def send_email( @@ -54,8 +61,15 @@ class SendGridEmailLogger(BaseEmailLogger): f"Sending email via SendGrid from {sender_email} to {to_email} with subject {subject}" ) + reply_to: Final = ( + {"reply_to": {"email": self.sendgrid_reply_to_email}} + if self.sendgrid_reply_to_email + else {} + ) + payload = { "from": {"email": sender_email}, + **reply_to, "personalizations": [ { "to": [{"email": email} for email in to_email], 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 index 465a03cfff7..6f2094717b0 100644 --- 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 @@ -133,3 +133,46 @@ async def test_send_email_multiple_recipients(mock_env_vars, mock_async_client): {"email": "recipient1@example.com"}, {"email": "recipient2@example.com"}, ] + + +@pytest.mark.asyncio +async def test_send_email_sets_reply_to_when_configured(monkeypatch, mock_async_client): + """SENDGRID_REPLY_TO_EMAIL is sent as the payload's reply_to and leaves from alone.""" + monkeypatch.setenv("SENDGRID_API_KEY", "test_api_key") + monkeypatch.delenv("SENDGRID_SENDER_EMAIL", raising=False) + monkeypatch.setenv("SENDGRID_REPLY_TO_EMAIL", "litellm-alerts@example.com") + + logger = SendGridEmailLogger() + logger.async_httpx_client = mock_async_client + + await logger.send_email( + from_email="no-reply@example.com", + to_email=["recipient@example.com"], + subject="Test Subject", + html_body="

Test email body

", + ) + + payload = mock_async_client.post.call_args[1]["json"] + assert payload["reply_to"] == {"email": "litellm-alerts@example.com"} + assert payload["from"] == {"email": "no-reply@example.com"} + + +@pytest.mark.asyncio +async def test_send_email_omits_reply_to_when_not_configured(monkeypatch, mock_async_client): + """With SENDGRID_REPLY_TO_EMAIL unset the key is absent, not null, so SendGrid + keeps defaulting Reply-To to the sender.""" + monkeypatch.setenv("SENDGRID_API_KEY", "test_api_key") + monkeypatch.delenv("SENDGRID_REPLY_TO_EMAIL", raising=False) + + logger = SendGridEmailLogger() + logger.async_httpx_client = mock_async_client + + await logger.send_email( + from_email="no-reply@example.com", + to_email=["recipient@example.com"], + subject="Test Subject", + html_body="

Test email body

", + ) + + payload = mock_async_client.post.call_args[1]["json"] + assert "reply_to" not in payload