CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-sendgrid-v5

Django email backend implementation compatible with SendGrid API v5+ for seamless email delivery integration

Pending
Overview
Eval results
Files

signals.mddocs/

Signals

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.

Capabilities

Email Sent Signal

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 failed

Signal Connection

Connect 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
    """

Usage Examples

Basic Signal Handling

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}")

Database Logging

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'
    )

Metrics and Analytics

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}", {})
    }

Error Handling and Alerting

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
    pass

Integration with External Services

Send 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
            continue

Signal Configuration

Best 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 decorator

Testing Signal Handlers

Test 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

docs

configuration.md

email-backend.md

index.md

signals.md

templates-personalization.md

webhooks.md

tile.json