Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generated by Django 5.0.14 on 2026-06-22

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("campaigns", "0009_campaign_cached_counters"),
("campaigns", "0009_campaignlead_bounce_metadata"),
]

operations = []
29 changes: 29 additions & 0 deletions backend/campaigns/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,35 @@ def test_unsubscribe_view_rejects_invalid_token(self):
lead.refresh_from_db()
self.assertFalse(lead.global_unsubscribe)

def test_unsubscribe_view_uses_organization_branding(self):
branded_org = Organization.objects.create(
name='Northwind',
unsubscribe_title='Stay connected with Northwind',
unsubscribe_message='You can change your preferences any time from our support team.',
brand_logo_url='https://cdn.example.com/northwind-logo.png',
)
branded_user = User.objects.create_user(
email='owner@northwind.test',
password='StrongPass123!',
organization=branded_org,
role='ADMIN',
)
lead = Lead.objects.create(
organization=branded_org,
email='unsubscribe@northwind.test',
)
token = generate_unsubscribe_token(lead.id)

self.client.force_authenticate(branded_user)
response = self.client.get(f'/api/v1/unsubscribe/{lead.id}/{token}/')

self.assertEqual(response.status_code, status.HTTP_200_OK)
html = response.content.decode('utf-8')
self.assertIn('Stay connected with Northwind', html)
self.assertIn('You can change your preferences any time from our support team.', html)
self.assertIn('https://cdn.example.com/northwind-logo.png', html)
self.assertIn('Northwind', html)

def test_send_email_step_skips_unsubscribed_leads(self):
campaign = Campaign.objects.create(
organization=self.organization,
Expand Down
52 changes: 45 additions & 7 deletions backend/campaigns/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging


from html import escape
import urllib.parse
from django.core.signing import Signer, BadSignature
from django.http import HttpResponseRedirect, HttpResponseBadRequest
Expand Down Expand Up @@ -718,21 +719,56 @@ def _build_fallback_content(self, request):
from .utils import verify_unsubscribe_token


def _unsubscribe_page(title, message, extra_html=''):
def _build_unsubscribe_brand_block(organization):
org_name = escape((organization.name or 'LeadOrbit').strip())
logo_url = (organization.brand_logo_url or '').strip()

if logo_url:
logo_html = (
f'<img class="brand-logo" src="{escape(logo_url, quote=True)}" '
f'alt="{org_name} logo">'
)
else:
logo_letter = escape((organization.name or 'L').strip()[:1] or 'L')
logo_html = f'<div class="brand-mark">{logo_letter}</div>'

return (
'<div class="brand">'
f'{logo_html}'
'<div>'
f'<div class="brand-name">{org_name}</div>'
'<div class="brand-subtitle">Email preferences</div>'
'</div>'
'</div>'
)


def _unsubscribe_page(organization, title, message, extra_html=''):
brand_title = escape((organization.unsubscribe_title or '').strip() or title)
brand_message = escape((organization.unsubscribe_message or '').strip() or message)
org_name = escape((organization.name or 'LeadOrbit').strip())
return (
'<!DOCTYPE html>'
'<html lang="en">'
'<head>'
'<meta charset="utf-8">'
'<meta name="viewport" content="width=device-width,initial-scale=1">'
f'<title>{title} | LeadOrbit</title>'
f'<title>{brand_title} | {org_name}</title>'
'<style>body{margin:0;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,sans-serif;background:#f8fafc;color:#111827;}'
'.container{max-width:720px;margin:72px auto;padding:32px;background:#ffffff;border:1px solid #e5e7eb;border-radius:24px;box-shadow:0 20px 80px rgba(15,23,42,.08);}'
'h1{margin-top:0;font-size:2rem;color:#0f172a;}p{font-size:1rem;line-height:1.7;color:#475569;}'
'.page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:40px 16px;}'
'.container{width:100%;max-width:760px;padding:32px;background:#ffffff;border:1px solid #e5e7eb;border-radius:28px;box-shadow:0 20px 80px rgba(15,23,42,.08);}'
'.brand{display:flex;align-items:center;gap:14px;margin-bottom:24px;}'
'.brand-mark{width:48px;height:48px;border-radius:14px;background:linear-gradient(135deg,#1d4ed8,#2563eb);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:1.1rem;}'
'.brand-logo{width:48px;height:48px;border-radius:14px;object-fit:cover;border:1px solid #e2e8f0;background:#fff;}'
'.brand-name{font-size:0.9rem;font-weight:800;color:#1d4ed8;text-transform:uppercase;letter-spacing:.08em;}'
'.brand-subtitle{font-size:.84rem;color:#64748b;margin-top:2px;}'
'h1{margin:0 0 14px;font-size:clamp(1.8rem,4vw,2.4rem);line-height:1.05;color:#0f172a;}'
'p{font-size:1rem;line-height:1.75;color:#475569;margin:0 0 16px;}'
'.note{margin-top:22px;padding:18px 20px;background:#f1f5f9;border-radius:18px;color:#334155;}'
'button{margin-top:12px;border:0;border-radius:999px;background:#1d4ed8;color:#fff;font-weight:700;padding:12px 20px;cursor:pointer;}'
'</style>'
'</head>'
f'<body><div class="container"><h1>{title}</h1><p>{message}</p>{extra_html}</div></body>'
f'<body><div class="page"><div class="container">{_build_unsubscribe_brand_block(organization)}<h1>{brand_title}</h1><p>{brand_message}</p>{extra_html}</div></div></body>'
'</html>'
)

Expand Down Expand Up @@ -764,6 +800,7 @@ def unsubscribe_view(request, lead_id, token):
'</form>'
)
html = _unsubscribe_page(
lead.organization,
'Confirm unsubscribe',
'Please confirm that you want to unsubscribe from future emails sent through LeadOrbit.',
form,
Expand All @@ -774,9 +811,10 @@ def unsubscribe_view(request, lead_id, token):
lead.save(update_fields=["global_unsubscribe"])

html = _unsubscribe_page(
lead.organization,
'Unsubscribed',
'You have been unsubscribed from all future emails sent through LeadOrbit.',
'<p>If you received this link by mistake, no further action is needed.</p>',
'<div class="note"><strong>Privacy note:</strong> Your unsubscribe preference has been recorded and will be respected across all future email campaigns.</div>',
)

return HttpResponse(html, content_type='text/html')
Expand Down Expand Up @@ -820,4 +858,4 @@ def get(self, request, *args, **kwargs):
# Original Destination par redirect karna
decoded_dest = urllib.parse.unquote(dest_url)
return HttpResponseRedirect(decoded_dest)
# ------------------------------------------
# ------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.0.14 on 2026-06-22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("tenants", "0002_organization_enable_ai_personalization_and_more"),
]

operations = [
migrations.AddField(
model_name="organization",
name="unsubscribe_title",
field=models.CharField(blank=True, default="", max_length=255),
),
migrations.AddField(
model_name="organization",
name="unsubscribe_message",
field=models.TextField(blank=True, default=""),
),
migrations.AddField(
model_name="organization",
name="brand_logo_url",
field=models.URLField(blank=True, default=""),
),
]
3 changes: 3 additions & 0 deletions backend/tenants/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class Organization(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
gemini_api_key = models.CharField(max_length=255, blank=True, null=True)
enable_ai_personalization = models.BooleanField(default=True)
unsubscribe_title = models.CharField(max_length=255, blank=True, default='')
unsubscribe_message = models.TextField(blank=True, default='')
brand_logo_url = models.URLField(blank=True, default='')

def __str__(self):
return self.name
Expand Down