Django email backend implementation compatible with SendGrid API v5+ for seamless email delivery integration
—
Django signal system integration for handling email delivery events, enabling custom logging, analytics, error tracking, and post-send processing. The package emits signals for both successful and failed email delivery attempts.
Django signal emitted after each email send attempt, providing information about success or failure.
sendgrid_email_sent = django.dispatch.Signal()
# Signal arguments:
# - sender: The SendgridBackend class
# - message: The EmailMessage object that was sent
# - fail_flag: Boolean indicating if the send failedConnect handlers to the SendGrid email signal for custom processing.
from sendgrid_backend.signals import sendgrid_email_sent
@receiver(sendgrid_email_sent)
def handle_sendgrid_email(sender, message, fail_flag, **kwargs):
"""
Handle SendGrid email send events.
Parameters:
- sender: SendgridBackend class (not instance)
- message: EmailMessage object with send details
- fail_flag: True if send failed, False if successful
- **kwargs: Additional signal data
"""Simple signal handler for logging email send events:
import logging
from django.dispatch import receiver
from sendgrid_backend.signals import sendgrid_email_sent
logger = logging.getLogger(__name__)
@receiver(sendgrid_email_sent)
def log_email_send(sender, message, fail_flag, **kwargs):
"""Log all email send attempts."""
status = "FAILED" if fail_flag else "SUCCESS"
recipients = ", ".join(message.to)
subject = message.subject
logger.info(f"Email {status}: '{subject}' to {recipients}")
if fail_flag:
logger.error(f"Email send failed for message: {subject}")
else:
# Log successful send with message ID if available
message_id = message.extra_headers.get('message_id')
if message_id:
logger.info(f"SendGrid message ID: {message_id}")Store email send events in database for analytics and tracking:
from django.db import models
from django.dispatch import receiver
from django.utils import timezone
from sendgrid_backend.signals import sendgrid_email_sent
class EmailSendLog(models.Model):
"""Model to track email send events."""
subject = models.CharField(max_length=255)
recipients = models.TextField() # JSON or comma-separated
sender = models.EmailField()
status = models.CharField(max_length=20)
sendgrid_message_id = models.CharField(max_length=100, blank=True)
send_time = models.DateTimeField(default=timezone.now)
failure_reason = models.TextField(blank=True)
# Additional tracking fields
template_id = models.CharField(max_length=50, blank=True)
categories = models.JSONField(default=list, blank=True)
custom_args = models.JSONField(default=dict, blank=True)
@receiver(sendgrid_email_sent)
def log_email_to_database(sender, message, fail_flag, **kwargs):
"""Store email send events in database."""
# Extract message details
recipients_list = message.to + message.cc + message.bcc
recipients = ",".join(recipients_list)
# Get SendGrid-specific data
template_id = getattr(message, 'template_id', '')
categories = getattr(message, 'categories', [])
custom_args = getattr(message, 'custom_args', {})
# Get message ID from headers (available after successful send)
message_id = message.extra_headers.get('message_id', '')
# Create log entry
EmailSendLog.objects.create(
subject=message.subject,
recipients=recipients,
sender=message.from_email,
status='failed' if fail_flag else 'sent',
sendgrid_message_id=message_id,
template_id=template_id,
categories=categories,
custom_args=custom_args,
failure_reason='' if not fail_flag else 'Send attempt failed'
)Collect metrics on email sending patterns and performance:
from django.core.cache import cache
from django.dispatch import receiver
from sendgrid_backend.signals import sendgrid_email_sent
import json
@receiver(sendgrid_email_sent)
def collect_email_metrics(sender, message, fail_flag, **kwargs):
"""Collect email sending metrics for analytics."""
# Increment counters
today = timezone.now().date().isoformat()
if fail_flag:
cache_key = f"email_failures_{today}"
cache.get_or_set(cache_key, 0, timeout=86400)
cache.incr(cache_key)
# Track failure reasons if available
failure_key = f"email_failure_reasons_{today}"
failures = cache.get(failure_key, {})
failure_type = "unknown" # Could extract from exception context
failures[failure_type] = failures.get(failure_type, 0) + 1
cache.set(failure_key, failures, timeout=86400)
else:
cache_key = f"email_successes_{today}"
cache.get_or_set(cache_key, 0, timeout=86400)
cache.incr(cache_key)
# Track successful sends by template if applicable
template_id = getattr(message, 'template_id', None)
if template_id:
template_key = f"template_usage_{today}"
usage = cache.get(template_key, {})
usage[template_id] = usage.get(template_id, 0) + 1
cache.set(template_key, usage, timeout=86400)
# Track recipient domains
domains_key = f"recipient_domains_{today}"
domains = cache.get(domains_key, {})
for email in message.to:
domain = email.split('@')[-1] if '@' in email else 'unknown'
domains[domain] = domains.get(domain, 0) + 1
cache.set(domains_key, domains, timeout=86400)
def get_email_metrics(date=None):
"""Retrieve email metrics for a specific date."""
if date is None:
date = timezone.now().date().isoformat()
return {
'successes': cache.get(f"email_successes_{date}", 0),
'failures': cache.get(f"email_failures_{date}", 0),
'failure_reasons': cache.get(f"email_failure_reasons_{date}", {}),
'template_usage': cache.get(f"template_usage_{date}", {}),
'recipient_domains': cache.get(f"recipient_domains_{date}", {})
}Set up alerts for email send failures and performance issues:
from django.dispatch import receiver
from django.core.mail import mail_administrators
from sendgrid_backend.signals import sendgrid_email_sent
import logging
logger = logging.getLogger(__name__)
@receiver(sendgrid_email_sent)
def handle_email_failures(sender, message, fail_flag, **kwargs):
"""Handle email send failures with alerting."""
if not fail_flag:
return # Only process failures
# Log the failure
logger.error(f"SendGrid email send failed: {message.subject}")
# Check failure rate
failure_rate = check_recent_failure_rate()
if failure_rate > 0.1: # More than 10% failure rate
send_admin_alert(f"High email failure rate: {failure_rate:.1%}")
# Alert for specific critical emails
is_critical = any(category in getattr(message, 'categories', [])
for category in ['critical', 'transactional', 'password-reset'])
if is_critical:
send_admin_alert(f"Critical email failed: {message.subject}")
# Store failed message for retry
store_failed_message(message)
def check_recent_failure_rate():
"""Calculate recent email failure rate."""
now = timezone.now()
hour_ago = now - timedelta(hours=1)
recent_logs = EmailSendLog.objects.filter(send_time__gte=hour_ago)
total = recent_logs.count()
if total == 0:
return 0
failures = recent_logs.filter(status='failed').count()
return failures / total
def send_admin_alert(message):
"""Send alert to administrators."""
mail_administrators(
subject='SendGrid Email Alert',
message=message,
fail_silently=True # Don't fail if admin email fails
)
def store_failed_message(message):
"""Store failed message for potential retry."""
# Implementation depends on your retry strategy
# Could store in database, queue, or cache
passSend email events to external analytics or monitoring services:
import requests
from django.dispatch import receiver
from django.conf import settings
from sendgrid_backend.signals import sendgrid_email_sent
@receiver(sendgrid_email_sent)
def send_to_analytics(sender, message, fail_flag, **kwargs):
"""Send email events to external analytics service."""
if not hasattr(settings, 'ANALYTICS_WEBHOOK_URL'):
return
# Prepare event data
event_data = {
'event_type': 'email_send_failed' if fail_flag else 'email_send_success',
'timestamp': timezone.now().isoformat(),
'email_subject': message.subject,
'recipient_count': len(message.to),
'template_id': getattr(message, 'template_id', None),
'categories': getattr(message, 'categories', []),
'custom_args': getattr(message, 'custom_args', {}),
}
# Add SendGrid message ID for successful sends
if not fail_flag:
event_data['sendgrid_message_id'] = message.extra_headers.get('message_id')
# Send to analytics service (async recommended for production)
try:
requests.post(
settings.ANALYTICS_WEBHOOK_URL,
json=event_data,
timeout=5
)
except requests.RequestException:
# Log but don't fail the email send process
logger.warning("Failed to send email event to analytics service")
@receiver(sendgrid_email_sent)
def update_user_email_status(sender, message, fail_flag, **kwargs):
"""Update user records with email delivery status."""
# Extract user information from message
user_emails = message.to + message.cc + message.bcc
for email in user_emails:
try:
# Update user's email status
user = User.objects.get(email=email)
if fail_flag:
# Increment failure count
profile = user.profile
profile.email_failures = profile.email_failures + 1
profile.last_email_failure = timezone.now()
profile.save()
# Disable email for high failure rates
if profile.email_failures > 5:
profile.email_enabled = False
profile.save()
else:
# Reset failure count on success
profile = user.profile
profile.email_failures = 0
profile.last_successful_email = timezone.now()
profile.save()
except User.DoesNotExist:
# Email not associated with user account
continueBest practices for configuring signal handlers:
# apps.py
from django.apps import AppConfig
class YourAppConfig(AppConfig):
name = 'your_app'
def ready(self):
# Import signal handlers to register them
import your_app.signals # noqa
# signals.py - organize all signal handlers in one module
from django.dispatch import receiver
from sendgrid_backend.signals import sendgrid_email_sent
# Import all your signal handlers
from .handlers.logging import log_email_send
from .handlers.metrics import collect_email_metrics
from .handlers.alerts import handle_email_failures
# Signal handlers are automatically registered via @receiver decoratorTest signal handlers to ensure proper functionality:
from django.test import TestCase
from django.core.mail import EmailMessage
from sendgrid_backend.signals import sendgrid_email_sent
class SignalHandlerTestCase(TestCase):
def test_email_send_signal(self):
"""Test that signal handlers are called correctly."""
# Create test message
message = EmailMessage(
subject='Test Email',
body='Test content',
from_email='test@example.com',
to=['recipient@example.com']
)
# Mock signal handler
handler_called = False
def test_handler(sender, message, fail_flag, **kwargs):
nonlocal handler_called
handler_called = True
self.assertFalse(fail_flag) # Should be successful
self.assertEqual(message.subject, 'Test Email')
# Connect test handler
sendgrid_email_sent.connect(test_handler)
try:
# Send signal manually for testing
sendgrid_email_sent.send(
sender=type(None),
message=message,
fail_flag=False
)
# Verify handler was called
self.assertTrue(handler_called)
finally:
# Disconnect test handler
sendgrid_email_sent.disconnect(test_handler)Install with Tessl CLI
npx tessl i tessl/pypi-django-sendgrid-v5