diff --git a/backend/campaigns/migrations/0010_merge_0009_campaign_cached_counters_0009_campaignlead_bounce_metadata.py b/backend/campaigns/migrations/0010_merge_0009_campaign_cached_counters_0009_campaignlead_bounce_metadata.py new file mode 100644 index 0000000..611e30b --- /dev/null +++ b/backend/campaigns/migrations/0010_merge_0009_campaign_cached_counters_0009_campaignlead_bounce_metadata.py @@ -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 = [] diff --git a/backend/campaigns/tests.py b/backend/campaigns/tests.py index 1d96075..ca9ded8 100644 --- a/backend/campaigns/tests.py +++ b/backend/campaigns/tests.py @@ -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, diff --git a/backend/campaigns/views.py b/backend/campaigns/views.py index 2a11bf0..774c2e4 100644 --- a/backend/campaigns/views.py +++ b/backend/campaigns/views.py @@ -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 @@ -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'' + ) + else: + logo_letter = escape((organization.name or 'L').strip()[:1] or 'L') + logo_html = f'
{logo_letter}
' + + return ( + '
' + f'{logo_html}' + '
' + f'
{org_name}
' + '
Email preferences
' + '
' + '
' + ) + + +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 ( '' '' '' '' '' - f'{title} | LeadOrbit' + f'{brand_title} | {org_name}' '' '' - f'

{title}

{message}

{extra_html}
' + f'
{_build_unsubscribe_brand_block(organization)}

{brand_title}

{brand_message}

{extra_html}
' '' ) @@ -764,6 +800,7 @@ def unsubscribe_view(request, lead_id, token): '' ) html = _unsubscribe_page( + lead.organization, 'Confirm unsubscribe', 'Please confirm that you want to unsubscribe from future emails sent through LeadOrbit.', form, @@ -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.', - '

If you received this link by mistake, no further action is needed.

', + '
Privacy note: Your unsubscribe preference has been recorded and will be respected across all future email campaigns.
', ) return HttpResponse(html, content_type='text/html') @@ -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) -# ------------------------------------------ \ No newline at end of file +# ------------------------------------------ diff --git a/backend/tenants/migrations/0003_organization_unsubscribe_branding.py b/backend/tenants/migrations/0003_organization_unsubscribe_branding.py new file mode 100644 index 0000000..9a120ab --- /dev/null +++ b/backend/tenants/migrations/0003_organization_unsubscribe_branding.py @@ -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=""), + ), + ] diff --git a/backend/tenants/models.py b/backend/tenants/models.py index bb58b37..1b40fae 100644 --- a/backend/tenants/models.py +++ b/backend/tenants/models.py @@ -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