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 unsentConfiguration
Django Settings
Add Unsent configuration to your Django settings:
# 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_EMAILEnvironment Variables
UNSENT_API_KEY=un_...
UNSENT_FROM_EMAIL=noreply@yourdomain.comQuick Start
Email Utility Module
Create a reusable email utility:
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
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
<!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
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 redisConfigure Celery
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}')from .celery import app as celery_app
__all__ = ('celery_app',)# 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
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
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 infoDjango Signals
Send emails automatically on model changes:
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:
from django.apps import AppConfig
class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
import myapp.signals # Import signalsManagement Commands
Create a command to send bulk emails:
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:
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:
passAdd URL pattern:
from django.urls import path
from . import views
urlpatterns = [
path('webhooks/unsent/', views.unsent_webhook, name='unsent_webhook'),
]Testing
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 testBest Practices
1. Create an EmailLog Model
Track all sent emails:
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:
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 responseAdd to settings:
MIDDLEWARE = [
'myapp.middleware.RequestMiddleware',
# ...other middleware
]3. Settings Validation
Validate configuration on startup:
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
- Python SDK: Explore all SDK features
- Flask Guide: Flask integration
- FastAPI Guide: Async email with FastAPI
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.