Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
892ba3d
feat: Add newsletter page with new releases (#3725)
dRaniwal Nov 29, 2025
c03abc0
Fix bot exclusions, add code reviewers section, and use timeuntil filter
dRaniwal Nov 29, 2025
0cbd748
Add open/closed bugs, domains, organizations to stats section
dRaniwal Dec 1, 2025
e5486e8
Remove unused context variables (open_bugs, closed_bugs, total_domain…
dRaniwal Dec 2, 2025
18a53c9
Merge branch 'main' into feat/newsletter-page-3725
DonnieBLT Dec 2, 2025
efd6bea
Pritz395 Dec 2, 2025
bd612f6
Pritz395 Dec 2, 2025
6fed376
Pritz395 Dec 2, 2025
8c7dcd0
Pritz395 Dec 2, 2025
9039df8
Pritz395 Dec 2, 2025
b50633b
Pritz395 Dec 2, 2025
8f9d817
Pritz395 Dec 2, 2025
519a0e8
Pritz395 Dec 2, 2025
5ab4aad
Pritz395 Dec 2, 2025
be64f1b
Pritz395 Dec 2, 2025
2285f26
Pritz395 Dec 2, 2025
36dc277
Pritz395 Dec 2, 2025
1b77cc8
Fix TransactionManagementError in IP restriction middleware
dRaniwal Dec 2, 2025
d887bc8
Update blt/middleware/ip_restrict.py
DonnieBLT Dec 2, 2025
a54678d
Initial plan
Copilot Nov 16, 2025
4c1bef7
Add newsletter feature files and configurations
Copilot Nov 16, 2025
f38b6c3
Fix syntax error in user.py - remove duplicate code
Copilot Nov 16, 2025
96efa19
Squash newsletter migrations into single file 0252
Copilot Nov 23, 2025
b626e70
Fix XSS vulnerabilities in newsletter feature by sanitizing markdown …
Copilot Nov 24, 2025
7a6f3c9
Add dark mode support to newsletter templates and fix pre-commit issues
Copilot Nov 27, 2025
29d9f03
Fix migration conflict: renumber newsletter migration from 0252 to 0259
Copilot Dec 2, 2025
bd65f2f
feat: Integrate email subscription system from PR #4847
dRaniwal Dec 2, 2025
8249156
fix: Address security vulnerabilities and code review issues
dRaniwal Dec 2, 2025
3ebb7b4
fix: address Copilot review comments
dRaniwal Dec 3, 2025
56528de
Resolve merge conflicts with upstream/main
dRaniwal Dec 3, 2025
8f09f13
fix: remove select_for_update() for SQLite test compatibility
dRaniwal Dec 3, 2025
5cc85ff
fix: improve exception handling with logger.exception() and proper ch…
dRaniwal Dec 3, 2025
9e15b5a
chore: revert bitcoin.py changes - out of scope for newsletter PR
dRaniwal Dec 3, 2025
8a1a0a5
fix: address race conditions and remove test.db
dRaniwal Dec 3, 2025
d5a08ae
fix: remove dead except block and simplify timing calculations
dRaniwal Dec 3, 2025
7dd88b6
Merge branch 'main' into feat/newsletter-page-3725
dRaniwal Dec 3, 2025
ac3b427
fix: replace get_object_or_404 in newsletter_unsubscribe
dRaniwal Dec 3, 2025
22e2bbb
fix: use timedelta from datetime module instead of timezone.timedelta
dRaniwal Dec 3, 2025
6762977
fix: add timing delay to success path to prevent email enumeration
dRaniwal Dec 3, 2025
5c74c60
Merge branch 'main' into feat/newsletter-page-3725
DonnieBLT Dec 3, 2025
24873df
Merge branch 'main' into feat/newsletter-page-3725
dRaniwal Dec 3, 2025
8417ae1
fix: resolve migration conflict with upstream
dRaniwal Dec 3, 2025
0b3d218
Merge branch 'main' into feat/newsletter-page-3725
dRaniwal Dec 3, 2025
d7d3386
fix: ensure Newsletter slug is always unique and non-empty
dRaniwal Dec 3, 2025
14b29bf
fix: use atomic QuerySet.update() in IP middleware to prevent Transac…
dRaniwal Dec 3, 2025
1f378f5
Merge branch 'main' into feat/newsletter-page-3725
DonnieBLT Dec 6, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ staticfiles
/env
/pvenv
db.sqlite3
*.db
.idea
/media
.vagrant
Expand Down
1 change: 1 addition & 0 deletions blt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.i18n",
"website.views.core.newsletter_context_processor",
],
"loaders": (
[
Expand Down
17 changes: 17 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
update_content_comment,
vote_count,
)
from website.views.newsletter import NewsletterView
from website.views.organization import (
BountyPayoutsView,
CreateHunt,
Expand Down Expand Up @@ -350,6 +351,13 @@
invite_friend,
mark_as_read,
messaging_home,
newsletter_confirm,
newsletter_detail,
newsletter_home,
newsletter_preferences,
newsletter_resend_confirmation,
newsletter_subscribe,
newsletter_unsubscribe,
profile,
profile_edit,
referral_signup,
Expand Down Expand Up @@ -622,6 +630,7 @@
EachmonthLeaderboardView.as_view(),
name="leaderboard_eachmonth",
),
re_path(r"^newsletter/$", NewsletterView.as_view(), name="newsletter"),
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL pattern conflict: The pattern re_path(r"^newsletter/$", ...) at line 632 will conflict with the more specific patterns added at lines 1222-1228.

The ^newsletter/$ pattern should be removed since:

  1. There's already a dedicated archive page at newsletter/archive/ (line 1222)
  2. The NewsletterView class-based view duplicates functionality with newsletter_home function-based view
  3. Having two URLs serving similar content is confusing for users and search engines

Recommendation: Remove line 632 and use only the function-based views, or consolidate all newsletter URLs to use either class-based or function-based views consistently. Based on the PR description mentioning /newsletter/ as the main page, you might want to change line 1222 from newsletter/archive/ to newsletter/ instead.

Suggested change
re_path(r"^newsletter/$", NewsletterView.as_view(), name="newsletter"),

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a URL pattern conflict. The pattern at line 633 uses re_path(r"^newsletter/$", ...) while line 1229 uses path("newsletter/<slug:slug>/", ...).

The regex pattern ^newsletter/$ will match /newsletter/ exactly, and the path pattern will match /newsletter/<anything>/. However, there are other newsletter URLs defined (lines 1223-1228) like /newsletter/archive/, /newsletter/subscribe/, etc.

The issue is that /newsletter/ goes to NewsletterView but users might expect it to show the newsletter archive. Consider:

  1. Making /newsletter/ redirect to /newsletter/archive/
  2. Or removing the /newsletter/ route and using /newsletter/archive/ as the main listing
  3. Or clarifying the distinction between the two views

The current setup may be confusing for users.

Suggested change
re_path(r"^newsletter/$", NewsletterView.as_view(), name="newsletter"),
re_path(r"^newsletter/$", RedirectView.as_view(url="/newsletter/archive/", permanent=False), name="newsletter"),

Copilot uses AI. Check for mistakes.
re_path(
r"^api/v1/issue/like/(?P<id>\w+)/$",
LikeIssueApiView.as_view(),
Expand Down Expand Up @@ -1210,6 +1219,14 @@
path("reminder-settings/", reminder_settings, name="reminder_settings"),
path("send-test-reminder/", send_test_reminder, name="send_test_reminder"),
path("check_domain_security_txt/", check_domain_security_txt, name="check_domain_security_txt"),
# Newsletter URLs
path("newsletter/archive/", newsletter_home, name="newsletter_home"),
path("newsletter/subscribe/", newsletter_subscribe, name="newsletter_subscribe"),
path("newsletter/confirm/<uuid:token>/", newsletter_confirm, name="newsletter_confirm"),
path("newsletter/unsubscribe/<uuid:token>/", newsletter_unsubscribe, name="newsletter_unsubscribe"),
path("newsletter/preferences/", newsletter_preferences, name="newsletter_preferences"),
path("newsletter/resend-confirmation/", newsletter_resend_confirmation, name="newsletter_resend_confirmation"),
path("newsletter/<slug:slug>/", newsletter_detail, name="newsletter_detail"),
path("bounty_payout/", bounty_payout, name="bounty_payout"),
Comment on lines +1229 to 1230
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL pattern for newsletter_detail at line 1229 (/newsletter/<slug:slug>/) must come after all other /newsletter/* patterns, otherwise it will match URLs like /newsletter/subscribe/, /newsletter/archive/, etc. and try to find a newsletter with slug "subscribe" or "archive". Move this pattern to the end of the newsletter URL group (after line 1228) to prevent it from capturing other newsletter URLs.

Suggested change
path("newsletter/<slug:slug>/", newsletter_detail, name="newsletter_detail"),
path("bounty_payout/", bounty_payout, name="bounty_payout"),
path("bounty_payout/", bounty_payout, name="bounty_payout"),
path("newsletter/<slug:slug>/", newsletter_detail, name="newsletter_detail"),

Copilot uses AI. Check for mistakes.
path("api/trademarks/search/", trademark_search_api, name="api_trademark_search"),
# Duplicate Bug Checking API
Expand Down
73 changes: 73 additions & 0 deletions website/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
ManagementCommandLog,
Message,
Monitor,
Newsletter,
NewsletterSubscriber,
Notification,
Organization,
OrganizationAdmin,
Expand Down Expand Up @@ -1099,6 +1101,77 @@ class UserBadgeAdmin(admin.ModelAdmin):
admin.site.register(UserBadge, UserBadgeAdmin)


@admin.register(Newsletter)
class NewsletterAdmin(admin.ModelAdmin):
list_display = ("title", "status", "published_at", "email_sent", "view_count")
list_filter = ("status", "email_sent")
search_fields = ("title", "content")
prepopulated_fields = {"slug": ("title",)}
readonly_fields = ("view_count", "email_sent_at")
date_hierarchy = "created_at"
fieldsets = (
("Content", {"fields": ("title", "slug", "content", "featured_image")}),
("Publication", {"fields": ("status", "published_at")}),
("Email Settings", {"fields": ("email_subject", "email_sent", "email_sent_at")}),
("Content Sections", {"fields": ("recent_bugs_section", "leaderboard_section", "reported_ips_section")}),
("Statistics", {"fields": ("view_count",)}),
)

actions = ["send_newsletter"]

def send_newsletter(self, request, queryset):
from django.core.management import call_command

count = 0
for newsletter in queryset:
if newsletter.status == "published" and not newsletter.email_sent:
call_command("send_newsletter", newsletter_id=newsletter.id)
count += 1

self.message_user(request, f"{count} newsletters were sent successfully.")

send_newsletter.short_description = "Send selected newsletters"


@admin.register(NewsletterSubscriber)
class NewsletterSubscriberAdmin(admin.ModelAdmin):
list_display = ("email", "name", "user", "subscription_status", "subscribed_at")
list_filter = ("is_active", "confirmed", "wants_bug_reports", "wants_leaderboard_updates", "wants_security_news")
search_fields = ("email", "name", "user__email", "user__username")
raw_id_fields = ("user",)
readonly_fields = ("confirmation_token",)

actions = ["send_confirmation_email", "mark_as_confirmed", "mark_as_unsubscribed"]

def subscription_status(self, obj):
return obj.subscription_status

def send_confirmation_email(self, request, queryset):
from website.views.user import send_confirmation_email

count = 0
for subscriber in queryset:
if not subscriber.confirmed and subscriber.is_active:
send_confirmation_email(subscriber)
count += 1

self.message_user(request, f"Confirmation emails sent to {count} subscribers.")

send_confirmation_email.short_description = "Send confirmation email"

def mark_as_confirmed(self, request, queryset):
queryset.update(confirmed=True)
self.message_user(request, f"{queryset.count()} subscribers marked as confirmed.")

mark_as_confirmed.short_description = "Mark selected subscribers as confirmed"

def mark_as_unsubscribed(self, request, queryset):
queryset.update(is_active=False)
self.message_user(request, f"{queryset.count()} subscribers marked as unsubscribed.")

mark_as_unsubscribed.short_description = "Mark selected subscribers as unsubscribed"


@admin.register(BannedApp)
class BannedAppAdmin(admin.ModelAdmin):
list_display = ("app_name", "country_name", "country_code", "app_type", "ban_date", "is_active")
Expand Down
124 changes: 124 additions & 0 deletions website/management/commands/send_newsletter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import logging

from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone

from website.models import Newsletter, NewsletterSubscriber

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Send published newsletter to subscribers"

def add_arguments(self, parser):
parser.add_argument("--newsletter_id", type=int, help="ID of the specific newsletter to send")
parser.add_argument("--test", action="store_true", help="Send a test email to the admin")

def handle(self, *args, **options):
newsletter_id = options.get("newsletter_id")
test_mode = options.get("test", False)

if newsletter_id:
# Send specific newsletter
try:
newsletter = Newsletter.objects.get(id=newsletter_id, status="published")
self.stdout.write(f"Preparing to send newsletter: {newsletter.title}")
self.send_newsletter(newsletter, test_mode)
except Newsletter.DoesNotExist:
self.stderr.write(f"Newsletter with ID {newsletter_id} does not exist or is not published")
else:
# Find newsletters that are published but not sent yet
newsletters = Newsletter.objects.filter(
status="published", email_sent=False, published_at__lte=timezone.now()
)

self.stdout.write(f"Found {newsletters.count()} newsletters to send")

for newsletter in newsletters:
self.send_newsletter(newsletter, test_mode)

def send_newsletter(self, newsletter, test_mode):
"""Send a specific newsletter to subscribers"""
if test_mode:
# Send only to admin email for testing
self.stdout.write(f"Sending test email for '{newsletter.title}' to admin")
if settings.ADMINS and len(settings.ADMINS) > 0:
self.send_to_subscriber(settings.ADMINS[0][1], newsletter, is_test=True)
else:
self.stderr.write("No admin email configured. Cannot send test email.")
return

# Get active, confirmed subscribers
subscribers = NewsletterSubscriber.objects.filter(is_active=True, confirmed=True)

if subscribers.exists():
self.stdout.write(f"Sending '{newsletter.title}' to {subscribers.count()} subscribers")

successful_sends = 0
for subscriber in subscribers:
try:
self.send_to_subscriber(subscriber.email, newsletter, subscriber=subscriber)
successful_sends += 1
except Exception as e:
logger.error(f"Failed to send newsletter to {subscriber.email}: {str(e)}")
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging exception details with str(e) violates the project's error handling guidelines. Use logger.error(f"Failed to send newsletter to {subscriber.email}", exc_info=True) instead to log the full exception trace without exposing exception details in the message string.

Suggested change
logger.error(f"Failed to send newsletter to {subscriber.email}: {str(e)}")
logger.error(f"Failed to send newsletter to {subscriber.email}", exc_info=True)

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +68
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newsletter sending implementation sends emails sequentially in a loop (lines 63-68). This can be very slow for a large subscriber base and may timeout. Consider using bulk email sending with Django's send_mass_mail() or implementing a celery task queue to handle email sending asynchronously in batches.

Copilot uses AI. Check for mistakes.

# Mark as sent if there were any successful sends
if successful_sends > 0:
newsletter.email_sent = True
newsletter.email_sent_at = timezone.now()
newsletter.save()

self.stdout.write(
self.style.SUCCESS(
f"Successfully sent newsletter '{newsletter.title}' to {successful_sends} subscribers"
)
)
else:
self.stdout.write(self.style.WARNING("No active subscribers found"))

def send_to_subscriber(self, email, newsletter, subscriber=None, is_test=False):
"""Send the newsletter to a specific subscriber"""
subject = newsletter.email_subject or f"{settings.PROJECT_NAME} Newsletter: {newsletter.title}"

if is_test:
subject = f"[TEST] {subject}"

# Build URL scheme based on settings
scheme = "https" if not settings.DEBUG else "http"

# Format newsletter content for email (converts markdown to sanitized HTML)
formatted_content = newsletter.format_for_email()

# Newsletter context
context = {
"newsletter": newsletter,
"newsletter_html_content": formatted_content["content"],
"subscriber": subscriber,
"unsubscribe_url": f"{scheme}://{settings.DOMAIN_NAME}"
+ reverse("newsletter_unsubscribe", args=[subscriber.confirmation_token])
if subscriber is not None
else "#",
Comment on lines +102 to +105
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unsubscribe URL construction will fail when subscriber is None (in test mode). The code checks if subscriber is not None after attempting to use subscriber.confirmation_token, which will raise an AttributeError. The ternary operator needs to be properly structured: reverse("newsletter_unsubscribe", args=[subscriber.confirmation_token]) if subscriber else "#" and the f-string should wrap the entire expression.

Suggested change
"unsubscribe_url": f"{scheme}://{settings.DOMAIN_NAME}"
+ reverse("newsletter_unsubscribe", args=[subscriber.confirmation_token])
if subscriber is not None
else "#",
"unsubscribe_url": (
f"{scheme}://{settings.DOMAIN_NAME}{reverse('newsletter_unsubscribe', args=[subscriber.confirmation_token])}"
if subscriber is not None
else "#"
),

Copilot uses AI. Check for mistakes.
"view_in_browser_url": f"{scheme}://{settings.DOMAIN_NAME}" + newsletter.get_absolute_url(),
Comment on lines +92 to +106
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheme determination uses settings.DEBUG to decide between http and https, which has the same issue as in other parts of the code. Production environments behind SSL-terminating proxies might have DEBUG=False but still need https URLs.

Additionally, this code accesses settings.DOMAIN_NAME directly without checking if it exists, which could raise an AttributeError.

Consider using a dedicated BASE_URL setting or falling back to the Site framework.

Copilot uses AI. Check for mistakes.
"project_name": settings.PROJECT_NAME,
"recent_bugs": newsletter.get_recent_bugs(),
"leaderboard": newsletter.get_leaderboard_updates(),
"reported_ips": newsletter.get_reported_ips(),
}

# Create HTML and plain text versions
html_content = render_to_string("newsletter/email/newsletter_email.html", context)
text_content = f"View this newsletter in your browser: {context['view_in_browser_url']}\n\n"
text_content += newsletter.content

# Create email message
email_message = EmailMultiAlternatives(
subject=subject, body=text_content, from_email=settings.DEFAULT_FROM_EMAIL, to=[email]
)

email_message.attach_alternative(html_content, "text/html")
email_message.send()
Comment on lines +15 to +124
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: The send_newsletter management command lacks automated tests. This is a critical function that sends emails to all subscribers and should be thoroughly tested.

Recommended tests:

  1. Test sending to specific newsletter by ID
  2. Test sending unpublished newsletters (should fail)
  3. Test test mode functionality
  4. Test handling of email send failures
  5. Test marking newsletters as sent
  6. Test with no active subscribers
  7. Test unsubscribe URL generation in emails

Add tests similar to test_weekly_bug_digest.py pattern.

Copilot uses AI. Check for mistakes.
85 changes: 85 additions & 0 deletions website/migrations/0260_newsletter_newslettersubscriber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Generated by Django 5.1.8 on 2025-12-02 16:33

import uuid

import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("website", "0259_add_search_history"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Newsletter",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=255)),
("slug", models.SlugField(unique=True)),
("content", mdeditor.fields.MDTextField(help_text="Write newsletter content in Markdown format")),
("featured_image", models.ImageField(blank=True, null=True, upload_to="newsletter_images")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("published_at", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[("draft", "Draft"), ("published", "Published")], default="draft", max_length=10
),
),
("recent_bugs_section", models.BooleanField(default=True, help_text="Include recently reported bugs")),
("leaderboard_section", models.BooleanField(default=True, help_text="Include leaderboard updates")),
("reported_ips_section", models.BooleanField(default=False, help_text="Include recently reported IPs")),
("email_subject", models.CharField(blank=True, max_length=255, null=True)),
("email_sent", models.BooleanField(default=False)),
("email_sent_at", models.DateTimeField(blank=True, null=True)),
("view_count", models.PositiveIntegerField(default=0)),
],
options={
"verbose_name": "Newsletter",
"verbose_name_plural": "Newsletters",
"ordering": ["-published_at"],
},
),
migrations.CreateModel(
name="NewsletterSubscriber",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("email", models.EmailField(max_length=254, unique=True)),
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The email field is defined with unique=True but the subscription logic in views expects to handle multiple subscription states for the same email. This will cause IntegrityError when users try to resubscribe. Consider making email non-unique or using a unique_together constraint with (email, is_active) to allow for proper subscription management.

Copilot uses AI. Check for mistakes.
("name", models.CharField(blank=True, max_length=100, null=True)),
("subscribed_at", models.DateTimeField(auto_now_add=True)),
("is_active", models.BooleanField(default=True)),
("confirmation_token", models.UUIDField(default=uuid.uuid4, editable=False)),
("confirmed", models.BooleanField(default=False)),
(
"token_created_at",
models.DateTimeField(
default=django.utils.timezone.now, help_text="Timestamp when confirmation token was created"
),
),
("wants_bug_reports", models.BooleanField(default=True)),
("wants_leaderboard_updates", models.BooleanField(default=True)),
("wants_security_news", models.BooleanField(default=True)),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="newsletter_subscriptions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Newsletter Subscriber",
"verbose_name_plural": "Newsletter Subscribers",
},
),
]
Loading
Loading