unsent
unsent.dev
Guides

Django

Complete guide to integrating Unsent with Django for email management

This guide shows you how to integrate Unsent into your Django application for sending transactional emails, managing contacts, and leveraging Django's powerful features.

Prerequisites

Python 3.8+: Django 4.x requires Python 3.8 or higher

Django 4.0+: This guide assumes Django 4.0 or newer

Unsent API Key: Generate one in your Unsent dashboard

Verified Domain: Set up a domain in the Domains section

Installation

Install the Unsent SDK:

pip install unsent

Configuration

Django Settings

Add Unsent configuration to your Django settings:

settings.py
# Unsent Email Configuration
UNSENT_API_KEY = env('UNSENT_API_KEY')
UNSENT_FROM_EMAIL = env('UNSENT_FROM_EMAIL', default='noreply@yourdomain.com')
UNSENT_BASE_URL = env('UNSENT_BASE_URL', default=None)  # Use for local development

# Optional: Configure default email backend
DEFAULT_FROM_EMAIL = UNSENT_FROM_EMAIL

Environment Variables

.env
UNSENT_API_KEY=un_...
UNSENT_FROM_EMAIL=noreply@yourdomain.com

Quick Start

Email Utility Module

Create a reusable email utility:

utils/email.py
from django.conf import settings
from unsent import unsent, types
from typing import Dict, Optional, Tuple
import logging

logger = logging.getLogger(__name__)

class UnsentClient:
    """Singleton Unsent client wrapper."""
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.client = unsent(
                settings.UNSENT_API_KEY,
                base_url=getattr(settings, 'UNSENT_BASE_URL', None),
                raise_on_error=False
            )
        return cls._instance
    
    def send_email(self, payload: types.EmailCreate) -> Tuple[Optional[Dict], Optional[Dict]]:
        """Send an email and log the result."""
        data, error = self.client.emails.send(payload)
        
        if error:
            logger.error(f"Failed to send email to {payload.get('to')}: {error}")
        else:
            logger.info(f"Email sent successfully: {data.get('id')}")
        
        return data, error

# Global instance
unsent_client = UnsentClient()

Send a Welcome Email

views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib import messages
from .forms import SignUpForm
from utils.email import unsent_client
from unsent import types

def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            
            # Send welcome email
            payload: types.EmailCreate = {
                "to": user.email,
                "from": settings.UNSENT_FROM_EMAIL,
                "subject": "Welcome to Our Platform!",
                "html": f"""
                    <h1>Welcome, {user.get_full_name() or user.username}!</h1>
                    <p>Thanks for joining us. We're excited to have you on board.</p>
                """,
                "text": f"Welcome, {user.get_full_name() or user.username}! Thanks for joining."
            }
            
            data, error = unsent_client.send_email(payload)
            
            if not error:
                messages.success(request, 'Account created! Check your email.')
            
            return redirect('home')
    else:
        form = SignUpForm()
    
    return render(request, 'signup.html', {'form': form})

Django Templates for Emails

Use Django's template system for emails:

Create Email Templates

templates/emails/welcome.html
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .header { background-color: #0070f3; color: white; padding: 20px; text-align: center; }
        .content { padding: 20px; background-color: #f9f9f9; }
        .button { display: inline-block; padding: 12px 24px; background-color: #0070f3; color: white; text-decoration: none; border-radius: 6px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Welcome to {{ site_name }}!</h1>
        </div>
        <div class="content">
            <p>Hi {{ user_name }},</p>
            <p>Thanks for joining {{ site_name }}! We're excited to have you on board.</p>
            <p>Here's what you can do next:</p>
            <ul>
                <li>Complete your profile</li>
                <li>Explore our features</li>
                <li>Connect with other users</li>
            </ul>
            <p style="text-align: center; margin-top: 30px;">
                <a href="{{ site_url }}" class="button">Get Started</a>
            </p>
        </div>
    </div>
</body>
</html>

Email Service Class

services/email_service.py
from django.template.loader import render_to_string
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from unsent import types
from utils.email import unsent_client
from typing import Dict, Optional, Tuple

class EmailService:
    """Service for sending emails using Django templates."""
    
    @staticmethod
    def send_welcome_email(user, request=None) -> Tuple[Optional[Dict], Optional[Dict]]:
        """Send welcome email to new user."""
        site = get_current_site(request) if request else None
        site_name = site.name if site else settings.SITE_NAME
        site_url = f"http://{site.domain}" if site else settings.SITE_URL
        
        context = {
            'user_name': user.get_full_name() or user.username,
            'site_name': site_name,
            'site_url': site_url
        }
        
        html = render_to_string('emails/welcome.html', context)
        
        payload: types.EmailCreate = {
            "to": user.email,
            "from": settings.UNSENT_FROM_EMAIL,
            "subject": f"Welcome to {site_name}!",
            "html": html,
            "text": f"Welcome to {site_name}, {context['user_name']}!"
        }
        
        return unsent_client.send_email(payload)
    
    @staticmethod
    def send_password_reset_email(user, reset_url) -> Tuple[Optional[Dict], Optional[Dict]]:
        """Send password reset email."""
        context = {
            'user_name': user.get_full_name() or user.username,
            'reset_url': reset_url,
            'site_name': settings.SITE_NAME
        }
        
        html = render_to_string('emails/password_reset.html', context)
        
        payload: types.EmailCreate = {
            "to": user.email,
            "from": settings.UNSENT_FROM_EMAIL,
            "subject": "Reset Your Password",
            "html": html,
            "text": f"Reset your password: {reset_url}"
        }
        
        return unsent_client.send_email(payload)

Celery Integration

Send emails asynchronously with Celery:

Install Celery

pip install celery redis

Configure Celery

myproject/celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

@app.task(bind=True)
def debug_task(self):
    print(f'Request: {self.request!r}')
myproject/__init__.py
from .celery import app as celery_app

__all__ = ('celery_app',)
settings.py
# Celery Configuration
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'

Create Email Tasks

myapp/tasks.py
from celery import shared_task
from django.contrib.auth import get_user_model
from services.email_service import EmailService
import logging

logger = logging.getLogger(__name__)
User = get_user_model()

@shared_task(bind=True, max_retries=3)
def send_welcome_email_task(self, user_id):
    """Celery task to send welcome email."""
    try:
        user = User.objects.get(pk=user_id)
        data, error = EmailService.send_welcome_email(user)
        
        if error:
            raise Exception(f"Failed to send email: {error}")
        
        return data
    except Exception as exc:
        logger.error(f"Error sending welcome email: {exc}")
        raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))

@shared_task
def send_bulk_emails_task(user_ids, subject, message):
    """Send emails to multiple users."""
    from utils.email import unsent_client
    from unsent import types
    
    users = User.objects.filter(pk__in=user_ids)
    
    batch_items = [
        {
            "to": user.email,
            "from": settings.UNSENT_FROM_EMAIL,
            "subject": subject,
            "html": message,
            "text": message
        }
        for user in users
    ]
    
    data, error = unsent_client.client.emails.batch(batch_items)
    return {"success": not error, "result": data}

Use Tasks in Views

views.py
from django.shortcuts import redirect
from django.contrib import messages
from .tasks import send_welcome_email_task

def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            user = form.save()
            
            # Send email asynchronously
            send_welcome_email_task.delay(user.id)
            
            messages.success(request, 'Account created!')
            return redirect('home')
    else:
        form = SignUpForm()
    
    return render(request, 'signup.html', {'form': form})

Run Celery Worker

celery -A myproject worker -l info

Django Signals

Send emails automatically on model changes:

signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from services.email_service import EmailService
import logging

logger = logging.getLogger(__name__)
User = get_user_model()

@receiver(post_save, sender=User)
def send_welcome_email_on_user_creation(sender, instance, created, **kwargs):
    """Send welcome email when new user is created."""
    if created and instance.email:
        try:
            EmailService.send_welcome_email(instance)
        except Exception as e:
            logger.error(f"Failed to send welcome email for user {instance.id}: {e}")

Register signals in your app config:

apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'myapp'
    
    def ready(self):
        import myapp.signals  # Import signals

Management Commands

Create a command to send bulk emails:

management/commands/send_newsletter.py
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from unsent import types
from utils.email import unsent_client
from django.conf import settings

User = get_user_model()

class Command(BaseCommand):
    help = 'Send newsletter to all active users'
    
    def add_arguments(self, parser):
        parser.add_argument('--subject', type=str, required=True)
        parser.add_argument('--template', type=str, required=True)
    
    def handle(self, *args, **options):
        subject = options['subject']
        template = options['template']
        
        # Get all active users with email
        users = User.objects.filter(is_active=True, email__isnull=False)
        
        self.stdout.write(f'Sending newsletter to {users.count()} users...')
        
        # Prepare batch
        batch_items = []
        for user in users:
            html = self.render_template(template, user)
            
            batch_items.append({
                "to": user.email,
                "from": settings.UNSENT_FROM_EMAIL,
                "subject": subject,
                "html": html
            })
            
            # Send in batches of 100
            if len(batch_items) >= 100:
                self.send_batch(batch_items)
                batch_items = []
        
        # Send remaining
        if batch_items:
            self.send_batch(batch_items)
        
        self.stdout.write(self.style.SUCCESS('Newsletter sent successfully!'))
    
    def render_template(self, template_name, user):
        from django.template.loader import render_to_string
        return render_to_string(template_name, {'user': user})
    
    def send_batch(self, batch_items):
        data, error = unsent_client.client.emails.batch(batch_items)
        if error:
            self.stdout.write(self.style.ERROR(f'Batch failed: {error}'))

Run the command:

python manage.py send_newsletter --subject "Weekly Newsletter" --template "emails/newsletter.html"

Webhooks

Handle Unsent webhooks in Django:

views.py
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.http import JsonResponse
import json
import logging

logger = logging.getLogger(__name__)

@csrf_exempt
@require_POST
def unsent_webhook(request):
    """Handle Unsent webhook events."""
    try:
        event = json.loads(request.body)
        event_type = event.get('type')
        data = event.get('data', {})
        
        # Route to appropriate handler
        handlers = {
            'email.delivered': handle_email_delivered,
            'email.bounced': handle_email_bounced,
            'email.opened': handle_email_opened,
            'email.clicked': handle_email_clicked,
            'email.complained': handle_email_complained
        }
        
        handler = handlers.get(event_type)
        if handler:
            handler(data)
        else:
            logger.info(f"Unhandled webhook event: {event_type}")
        
        return JsonResponse({"status": "received"})
    
    except Exception as e:
        logger.error(f"Webhook error: {e}")
        return JsonResponse({"error": str(e)}, status=500)

def handle_email_delivered(data):
    """Handle email delivered event."""
    email_id = data.get('emailId')
    recipient = data.get('recipient')
    logger.info(f"Email {email_id} delivered to {recipient}")
    # Update EmailLog model, track analytics

def handle_email_bounced(data):
    """Handle email bounced event."""
    from django.contrib.auth import get_user_model
    User = get_user_model()
    
    recipient = data.get('recipient')
    bounce_type = data.get('bounceType')
    
    logger.warning(f"Email bounced: {recipient} ({bounce_type})")
    
    # Mark user email as bounced
    try:
        user = User.objects.get(email=recipient)
        user.email_bounced = True
        user.save()
    except User.DoesNotExist:
        pass

def handle_email_opened(data):
    """Handle email opened event."""
    email_id = data.get('emailId')
    logger.info(f"Email {email_id} opened")
    # Track open rate in analytics

def handle_email_clicked(data):
    """Handle link clicked event."""
    email_id = data.get('emailId')
    link = data.get('link')
    logger.info(f"Link clicked in {email_id}: {link}")
    # Track click rate

def handle_email_complained(data):
    """Handle spam complaint."""
    from django.contrib.auth import get_user_model
    User = get_user_model()
    
    recipient = data.get('recipient')
    logger.warning(f"Spam complaint from {recipient}")
    
    # Automatically unsubscribe
    try:
        user = User.objects.get(email=recipient)
        user.email_subscribed = False
        user.save()
    except User.DoesNotExist:
        pass

Add URL pattern:

urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('webhooks/unsent/', views.unsent_webhook, name='unsent_webhook'),
]

Testing

tests.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from unittest.mock import patch, Mock
from services.email_service import EmailService

User = get_user_model()

class EmailServiceTestCase(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            first_name='Test',
            last_name='User'
        )
    
    @patch('utils.email.unsent_client.client')
    def test_send_welcome_email_success(self, mock_client):
        """Test successful welcome email send."""
        mock_client.emails.send.return_value = ({"id": "email_123"}, None)
        
        data, error = EmailService.send_welcome_email(self.user)
        
        self.assertIsNone(error)
        self.assertEqual(data['id'], 'email_123')
        mock_client.emails.send.assert_called_once()
    
    @patch('utils.email.unsent_client.client')
    def test_send_welcome_email_failure(self, mock_client):
        """Test failed welcome email send."""
        mock_client.emails.send.return_value = (None, {"message": "API Error"})
        
        data, error = EmailService.send_welcome_email(self.user)
        
        self.assertIsNone(data)
        self.assertIsNotNone(error)
        self.assertEqual(error['message'], 'API Error')

class WebhookTestCase(TestCase):
    def test_email_delivered_webhook(self):
        """Test email delivered webhook processing."""
        payload = {
            "type": "email.delivered",
            "data": {
                "emailId": "email_123",
                "recipient": "test@example.com"
            }
        }
        
        response = self.client.post(
            '/webhooks/unsent/',
            data=payload,
            content_type='application/json'
        )
        
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()['status'], 'received')

Run tests:

python manage.py test

Best Practices

1. Create an EmailLog Model

Track all sent emails:

models.py
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class EmailLog(models.Model):
    STATUS_CHOICES = [
        ('pending', 'Pending'),
        ('sent', 'Sent'),
        ('delivered', 'Delivered'),
        ('bounced', 'Bounced'),
        ('complained', 'Complained'),
    ]
    
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
    unsent_id = models.CharField(max_length=100, unique=True)
    recipient = models.EmailField()
    subject = models.CharField(max_length=200)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    sent_at = models.DateTimeField(auto_now_add=True)
    delivered_at = models.DateTimeField(null=True, blank=True)
    opened_at = models.DateTimeField(null=True, blank=True)
    opened_count = models.IntegerField(default=0)
    clicked_count = models.IntegerField(default=0)
    
    class Meta:
        ordering = ['-sent_at']
        indexes = [
            models.Index(fields=['unsent_id']),
            models.Index(fields=['recipient']),
            models.Index(fields=['status']),
        ]
    
    def __str__(self):
        return f"{self.subject} to {self.recipient}"

2. Middleware for Request Context

Access request in email service:

middleware.py
import threading

_thread_locals = threading.local()

def get_current_request():
    return getattr(_thread_locals, 'request', None)

class RequestMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        _thread_locals.request = request
        response = self.get_response(request)
        _thread_locals.request = None
        return response

Add to settings:

MIDDLEWARE = [
    'myapp.middleware.RequestMiddleware',
    # ...other middleware
]

3. Settings Validation

Validate configuration on startup:

apps.py
from django.apps import AppConfig
from django.core.exceptions import ImproperlyConfigured

class MyAppConfig(AppConfig):
    name = 'myapp'
    
    def ready(self):
        from django.conf import settings
        
        if not hasattr(settings, 'UNSENT_API_KEY'):
            raise ImproperlyConfigured('UNSENT_API_KEY is required')
        
        if not settings.UNSENT_API_KEY:
            raise ImproperlyConfigured('UNSENT_API_KEY cannot be empty')

Next Steps

Django's admin interface is perfect for managing email logs, contacts, and campaigns. Create ModelAdmin classes for your email-related models to get a powerful management interface.