feat(sendgrid): support an optional SENDGRID_REPLY_TO_EMAIL on outbound emails

This commit is contained in:
michelligabriele 2026-09-04 13:20:58 +02:00
parent c8635ecc67
commit 8fc119058e
No known key found for this signature in database
2 changed files with 58 additions and 1 deletions

View file

@ -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],

View file

@ -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="<p>Test email body</p>",
)
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="<p>Test email body</p>",
)
payload = mock_async_client.post.call_args[1]["json"]
assert "reply_to" not in payload