Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -1,36 +1,39 @@
from django.contrib import admin
from django.contrib.sites.models import Site
from django.contrib.sites.shortcuts import get_current_site
from .models import EmailConfiguration
@admin.register(EmailConfiguration)
class EmailConfigurationAdmin(admin.ModelAdmin):
list_display = ('site', 'from_name', 'from_email', 'reply_to', 'updated_at')
list_select_related = ('site',)
search_fields = ('site__domain', 'from_name', 'from_email', 'reply_to')
readonly_fields = ('created_at', 'updated_at')
list_display = (
"site",
"from_name",
"from_email",
"reply_to",
"updated_at",
)
list_select_related = ("site",)
search_fields = ("site__domain", "from_name", "from_email", "reply_to")
readonly_fields = ("created_at", "updated_at")
fieldsets = (
(None, {
'fields': ('site',)
}),
('Email Settings', {
'fields': (
'api_key',
('from_name', 'from_email'),
'reply_to'
),
'description': 'Configure the email settings. The From field in emails will appear as "From Name <from@email.com>"'
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
})
(None, {"fields": ("site",)}),
(
"Email Settings",
{
"fields": ("api_key", ("from_name", "from_email"), "reply_to"),
"description": 'Configure the email settings. The From field in emails will appear as "From Name <from@email.com>"',
},
),
(
"Timestamps",
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
),
)
def get_queryset(self, request):
return super().get_queryset(request).select_related('site')
return super().get_queryset(request).select_related("site")
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "site":
kwargs["queryset"] = Site.objects.all().order_by('domain')
kwargs["queryset"] = Site.objects.all().order_by("domain")
return super().formfield_for_foreignkey(db_field, request, **kwargs)

View File

@@ -1,13 +1,13 @@
from django.core.mail.backends.base import BaseEmailBackend
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail.message import sanitize_address
from .services import EmailService
from .models import EmailConfiguration
class ForwardEmailBackend(BaseEmailBackend):
def __init__(self, fail_silently=False, **kwargs):
super().__init__(fail_silently=fail_silently)
self.site = kwargs.get('site', None)
self.site = kwargs.get("site", None)
def send_messages(self, email_messages):
"""
@@ -23,7 +23,7 @@ class ForwardEmailBackend(BaseEmailBackend):
sent = self._send(message)
if sent:
num_sent += 1
except Exception as e:
except Exception:
if not self.fail_silently:
raise
return num_sent
@@ -33,11 +33,14 @@ class ForwardEmailBackend(BaseEmailBackend):
if not email_message.recipients():
return False
# Get the first recipient (ForwardEmail API sends to one recipient at a time)
# Get the first recipient (ForwardEmail API sends to one recipient at a
# time)
to_email = email_message.to[0]
# Get site from connection or instance
if hasattr(email_message, 'connection') and hasattr(email_message.connection, 'site'):
if hasattr(email_message, "connection") and hasattr(
email_message.connection, "site"
):
site = email_message.connection.site
else:
site = self.site
@@ -49,11 +52,16 @@ class ForwardEmailBackend(BaseEmailBackend):
try:
config = EmailConfiguration.objects.get(site=site)
except EmailConfiguration.DoesNotExist:
raise ValueError(f"Email configuration not found for site: {site.domain}")
raise ValueError(
f"Email configuration not found for site: {
site.domain}"
)
# Get the from email, falling back to site's default if not provided
if email_message.from_email:
from_email = sanitize_address(email_message.from_email, email_message.encoding)
from_email = sanitize_address(
email_message.from_email, email_message.encoding
)
else:
from_email = config.default_from_email
@@ -62,13 +70,16 @@ class ForwardEmailBackend(BaseEmailBackend):
# Get reply-to from message headers or use default
reply_to = None
if hasattr(email_message, 'reply_to') and email_message.reply_to:
if hasattr(email_message, "reply_to") and email_message.reply_to:
reply_to = email_message.reply_to[0]
elif hasattr(email_message, 'extra_headers') and 'Reply-To' in email_message.extra_headers:
reply_to = email_message.extra_headers['Reply-To']
elif (
hasattr(email_message, "extra_headers")
and "Reply-To" in email_message.extra_headers
):
reply_to = email_message.extra_headers["Reply-To"]
# Get message content
if email_message.content_subtype == 'html':
if email_message.content_subtype == "html":
# If it's HTML content, we'll send it as text for now
# You could extend this to support HTML emails if needed
text = email_message.body
@@ -82,10 +93,10 @@ class ForwardEmailBackend(BaseEmailBackend):
text=text,
from_email=from_email,
reply_to=reply_to,
site=site
site=site,
)
return True
except Exception as e:
except Exception:
if not self.fail_silently:
raise
return False

View File

@@ -4,53 +4,51 @@ from django.contrib.sites.models import Site
from django.test import RequestFactory, Client
from allauth.account.models import EmailAddress
from accounts.adapters import CustomAccountAdapter
from email_service.services import EmailService
from django.conf import settings
import uuid
User = get_user_model()
class Command(BaseCommand):
help = 'Test all email flows in the application'
help = "Test all email flows in the application"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.factory = RequestFactory()
self.client = Client(enforce_csrf_checks=False) # Disable CSRF for testing
# Disable CSRF for testing
self.client = Client(enforce_csrf_checks=False)
self.adapter = CustomAccountAdapter()
self.site = Site.objects.get_current()
# Generate unique test data
unique_id = str(uuid.uuid4())[:8]
self.test_username = f'testuser_{unique_id}'
self.test_email = f'test_{unique_id}@thrillwiki.com'
self.test_[PASSWORD-REMOVED]"
self.new_[PASSWORD-REMOVED]"
self.test_username = f"testuser_{unique_id}"
self.test_email = f"test_{unique_id}@thrillwiki.com"
self.test_password = "[PASSWORD-REMOVED]"
self.new_password = "[PASSWORD-REMOVED]"
# Add testserver to ALLOWED_HOSTS
if 'testserver' not in settings.ALLOWED_HOSTS:
settings.ALLOWED_HOSTS.append('testserver')
if "testserver" not in settings.ALLOWED_HOSTS:
settings.ALLOWED_HOSTS.append("testserver")
def handle(self, *args, **options):
self.stdout.write('Starting email flow tests...\n')
self.stdout.write("Starting email flow tests...\n")
# Clean up any existing test users
User.objects.filter(email__endswith='@thrillwiki.com').delete()
User.objects.filter(email__endswith="@thrillwiki.com").delete()
# Test registration email
self.test_registration()
# Create a test user for other flows
user = User.objects.create_user(
username=f'testuser2_{str(uuid.uuid4())[:8]}',
email=f'test2_{str(uuid.uuid4())[:8]}@thrillwiki.com',
password=self.test_password
username=f"testuser2_{str(uuid.uuid4())[:8]}",
email=f"test2_{str(uuid.uuid4())[:8]}@thrillwiki.com",
password=self.test_password,
)
EmailAddress.objects.create(
user=user,
email=user.email,
primary=True,
verified=True
user=user, email=user.email, primary=True, verified=True
)
# Log in the test user
@@ -62,89 +60,137 @@ class Command(BaseCommand):
self.test_password_reset(user)
# Cleanup
User.objects.filter(email__endswith='@thrillwiki.com').delete()
self.stdout.write(self.style.SUCCESS('All email flow tests completed!\n'))
User.objects.filter(email__endswith="@thrillwiki.com").delete()
self.stdout.write(self.style.SUCCESS("All email flow tests completed!\n"))
def test_registration(self):
"""Test registration email flow"""
self.stdout.write('Testing registration email...')
self.stdout.write("Testing registration email...")
try:
# Use dj-rest-auth registration endpoint
response = self.client.post('/api/auth/registration/', {
'username': self.test_username,
'email': self.test_email,
'password1': self.test_password,
'password2': self.test_password
}, content_type='application/json')
response = self.client.post(
"/api/auth/registration/",
{
"username": self.test_username,
"email": self.test_email,
"password1": self.test_password,
"password2": self.test_password,
},
content_type="application/json",
)
if response.status_code in [200, 201, 204]:
self.stdout.write(self.style.SUCCESS('Registration email test passed!\n'))
self.stdout.write(
self.style.SUCCESS("Registration email test passed!\n")
)
else:
self.stdout.write(
self.style.WARNING(
f'Registration returned status {response.status_code}: {response.content.decode()}\n'
f"Registration returned status {
response.status_code}: {
response.content.decode()}\n"
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(f'Registration email test failed: {str(e)}\n'))
self.stdout.write(
self.style.ERROR(
f"Registration email test failed: {
str(e)}\n"
)
)
def test_password_change(self, user):
"""Test password change using dj-rest-auth"""
self.stdout.write('Testing password change email...')
self.stdout.write("Testing password change email...")
try:
response = self.client.post('/api/auth/password/change/', {
'old_password': self.test_password,
'new_password1': self.new_password,
'new_password2': self.new_password
}, content_type='application/json')
response = self.client.post(
"/api/auth/password/change/",
{
"old_password": self.test_password,
"new_password1": self.new_password,
"new_password2": self.new_password,
},
content_type="application/json",
)
if response.status_code == 200:
self.stdout.write(self.style.SUCCESS('Password change email test passed!\n'))
self.stdout.write(
self.style.SUCCESS("Password change email test passed!\n")
)
else:
self.stdout.write(
self.style.WARNING(
f'Password change returned status {response.status_code}: {response.content.decode()}\n'
f"Password change returned status {
response.status_code}: {
response.content.decode()}\n"
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(f'Password change email test failed: {str(e)}\n'))
self.stdout.write(
self.style.ERROR(
f"Password change email test failed: {
str(e)}\n"
)
)
def test_email_change(self, user):
"""Test email change verification"""
self.stdout.write('Testing email change verification...')
self.stdout.write("Testing email change verification...")
try:
new_email = f'newemail_{str(uuid.uuid4())[:8]}@thrillwiki.com'
response = self.client.post('/api/auth/email/', {
'email': new_email
}, content_type='application/json')
new_email = f"newemail_{str(uuid.uuid4())[:8]}@thrillwiki.com"
response = self.client.post(
"/api/auth/email/",
{"email": new_email},
content_type="application/json",
)
if response.status_code == 200:
self.stdout.write(self.style.SUCCESS('Email change verification test passed!\n'))
self.stdout.write(
self.style.SUCCESS("Email change verification test passed!\n")
)
else:
self.stdout.write(
self.style.WARNING(
f'Email change returned status {response.status_code}: {response.content.decode()}\n'
f"Email change returned status {
response.status_code}: {
response.content.decode()}\n"
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(f'Email change verification test failed: {str(e)}\n'))
self.stdout.write(
self.style.ERROR(
f"Email change verification test failed: {
str(e)}\n"
)
)
def test_password_reset(self, user):
"""Test password reset using dj-rest-auth"""
self.stdout.write('Testing password reset email...')
self.stdout.write("Testing password reset email...")
try:
# Request password reset
response = self.client.post('/api/auth/password/reset/', {
'email': user.email
}, content_type='application/json')
response = self.client.post(
"/api/auth/password/reset/",
{"email": user.email},
content_type="application/json",
)
if response.status_code == 200:
self.stdout.write(self.style.SUCCESS('Password reset email test passed!\n'))
self.stdout.write(
self.style.SUCCESS("Password reset email test passed!\n")
)
else:
self.stdout.write(
self.style.WARNING(
f'Password reset returned status {response.status_code}: {response.content.decode()}\n'
f"Password reset returned status {
response.status_code}: {
response.content.decode()}\n"
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(f'Password reset email test failed: {str(e)}\n'))
self.stdout.write(
self.style.ERROR(
f"Password reset email test failed: {
str(e)}\n"
)
)

View File

@@ -1,32 +1,32 @@
from django.core.management.base import BaseCommand
from django.contrib.sites.models import Site
from django.core.mail import send_mail, get_connection
from django.core.mail import send_mail
from django.conf import settings
import requests
import json
import os
from email_service.models import EmailConfiguration
from email_service.services import EmailService
from email_service.backends import ForwardEmailBackend
class Command(BaseCommand):
help = 'Test the email service functionality'
help = "Test the email service functionality"
def add_arguments(self, parser):
parser.add_argument(
'--to',
"--to",
type=str,
help='Recipient email address (optional, defaults to current user\'s email)',
help="Recipient email address (optional, defaults to current user's email)",
)
parser.add_argument(
'--api-key',
"--api-key",
type=str,
help='ForwardEmail API key (optional, will use configured value)',
help="ForwardEmail API key (optional, will use configured value)",
)
parser.add_argument(
'--from-email',
"--from-email",
type=str,
help='Sender email address (optional, will use configured value)',
help="Sender email address (optional, will use configured value)",
)
def get_config(self):
@@ -35,58 +35,62 @@ class Command(BaseCommand):
site = Site.objects.get(id=settings.SITE_ID)
config = EmailConfiguration.objects.get(site=site)
return {
'api_key': config.api_key,
'from_email': config.default_from_email,
'site': site
"api_key": config.api_key,
"from_email": config.default_from_email,
"site": site,
}
except (Site.DoesNotExist, EmailConfiguration.DoesNotExist):
# Try environment variables
api_key = os***REMOVED***iron.get('FORWARD_EMAIL_API_KEY')
from_email = os***REMOVED***iron.get('FORWARD_EMAIL_FROM')
api_key = os.environ.get("FORWARD_EMAIL_API_KEY")
from_email = os.environ.get("FORWARD_EMAIL_FROM")
if not api_key or not from_email:
self.stdout.write(self.style.WARNING(
'No configuration found in database or environment variables.\n'
'Please either:\n'
'1. Configure email settings in Django admin, or\n'
'2. Set environment variables FORWARD_EMAIL_API_KEY and FORWARD_EMAIL_FROM, or\n'
'3. Provide --api-key and --from-email arguments'
))
self.stdout.write(
self.style.WARNING(
"No configuration found in database or environment variables.\n"
"Please either:\n"
"1. Configure email settings in Django admin, or\n"
"2. Set environment variables FORWARD_EMAIL_API_KEY and FORWARD_EMAIL_FROM, or\n"
"3. Provide --api-key and --from-email arguments"
)
)
return None
return {
'api_key': api_key,
'from_email': from_email,
'site': Site.objects.get(id=settings.SITE_ID)
"api_key": api_key,
"from_email": from_email,
"site": Site.objects.get(id=settings.SITE_ID),
}
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Starting email service tests...'))
self.stdout.write(self.style.SUCCESS("Starting email service tests..."))
# Get configuration
config = self.get_config()
if not config and not (options['api_key'] and options['from_email']):
self.stdout.write(self.style.ERROR('No email configuration available. Tests aborted.'))
if not config and not (options["api_key"] and options["from_email"]):
self.stdout.write(
self.style.ERROR("No email configuration available. Tests aborted.")
)
return
# Use provided values or fall back to config
api_key = options['api_key'] or config['api_key']
from_email = options['from_email'] or config['from_email']
site = config['site']
api_key = options["api_key"] or config["api_key"]
from_email = options["from_email"] or config["from_email"]
site = config["site"]
# If no recipient specified, use the from_email address for testing
to_email = options['to'] or 'test@thrillwiki.com'
to_email = options["to"] or "test@thrillwiki.com"
self.stdout.write(self.style.SUCCESS('Using configuration:'))
self.stdout.write(f' From: {from_email}')
self.stdout.write(f' To: {to_email}')
self.stdout.write(self.style.SUCCESS("Using configuration:"))
self.stdout.write(f" From: {from_email}")
self.stdout.write(f" To: {to_email}")
self.stdout.write(f' API Key: {"*" * len(api_key)}')
self.stdout.write(f' Site: {site.domain}')
self.stdout.write(f" Site: {site.domain}")
try:
# 1. Test site configuration
config = self.test_site_configuration(api_key, from_email)
# 2. Test direct service
self.test_email_service_directly(to_email, config.site)
@@ -96,118 +100,145 @@ class Command(BaseCommand):
# 4. Test Django email backend
self.test_email_backend(to_email, config.site)
self.stdout.write(self.style.SUCCESS('\nAll tests completed successfully! 🎉'))
self.stdout.write(
self.style.SUCCESS("\nAll tests completed successfully! 🎉")
)
except Exception as e:
self.stdout.write(self.style.ERROR(f'\nTests failed: {str(e)}'))
self.stdout.write(self.style.ERROR(f"\nTests failed: {str(e)}"))
def test_site_configuration(self, api_key, from_email):
"""Test creating and retrieving site configuration"""
self.stdout.write('\nTesting site configuration...')
self.stdout.write("\nTesting site configuration...")
try:
# Get or create default site
site = Site.objects.get_or_create(
id=settings.SITE_ID,
defaults={
'domain': 'example.com',
'name': 'example.com'
}
defaults={"domain": "example.com", "name": "example.com"},
)[0]
# Create or update email configuration
config, created = EmailConfiguration.objects.update_or_create(
site=site,
defaults={
'api_key': api_key,
'default_from_email': from_email
}
"api_key": api_key,
"default_from_email": from_email,
},
)
action = 'Created new' if created else 'Updated existing'
self.stdout.write(self.style.SUCCESS(f'{action} site configuration'))
action = "Created new" if created else "Updated existing"
self.stdout.write(self.style.SUCCESS(f"{action} site configuration"))
return config
except Exception as e:
self.stdout.write(self.style.ERROR(f'✗ Site configuration failed: {str(e)}'))
self.stdout.write(
self.style.ERROR(
f"✗ Site configuration failed: {
str(e)}"
)
)
raise
def test_api_endpoint(self, to_email):
"""Test sending email via the API endpoint"""
self.stdout.write('\nTesting API endpoint...')
self.stdout.write("\nTesting API endpoint...")
try:
# Make request to the API endpoint
response = requests.post(
'http://127.0.0.1:8000/api/email/send-email/',
"http://127.0.0.1:8000/api/email/send-email/",
json={
'to': to_email,
'subject': 'Test Email via API',
'text': 'This is a test email sent via the API endpoint.'
"to": to_email,
"subject": "Test Email via API",
"text": "This is a test email sent via the API endpoint.",
},
headers={
'Content-Type': 'application/json',
},
timeout=60)
"Content-Type": "application/json",
},
timeout=60,
)
if response.status_code == 200:
self.stdout.write(self.style.SUCCESS('✓ API endpoint test successful'))
self.stdout.write(self.style.SUCCESS("✓ API endpoint test successful"))
else:
self.stdout.write(
self.style.ERROR(
f'✗ API endpoint test failed with status {response.status_code}: {response.text}'
f"✗ API endpoint test failed with status {
response.status_code}: {
response.text}"
)
)
raise Exception(f"API test failed: {response.text}")
except requests.exceptions.ConnectionError:
self.stdout.write(
self.style.ERROR(
'✗ API endpoint test failed: Could not connect to server. '
'Make sure the Django development server is running.'
"✗ API endpoint test failed: Could not connect to server. "
"Make sure the Django development server is running."
)
)
raise Exception("Could not connect to Django server")
except Exception as e:
self.stdout.write(self.style.ERROR(f'✗ API endpoint test failed: {str(e)}'))
self.stdout.write(
self.style.ERROR(
f"✗ API endpoint test failed: {
str(e)}"
)
)
raise
def test_email_backend(self, to_email, site):
"""Test sending email via Django's email backend"""
self.stdout.write('\nTesting Django email backend...')
self.stdout.write("\nTesting Django email backend...")
try:
# Create a connection with site context
backend = ForwardEmailBackend(fail_silently=False, site=site)
# Debug output
self.stdout.write(f' Debug: Using from_email: {site.email_config.default_from_email}')
self.stdout.write(f' Debug: Using to_email: {to_email}')
self.stdout.write(
f" Debug: Using from_email: {
site.email_config.default_from_email}"
)
self.stdout.write(f" Debug: Using to_email: {to_email}")
send_mail(
subject='Test Email via Backend',
message='This is a test email sent via the Django email backend.',
subject="Test Email via Backend",
message="This is a test email sent via the Django email backend.",
from_email=site.email_config.default_from_email, # Explicitly set from_email
recipient_list=[to_email],
fail_silently=False,
connection=backend
connection=backend,
)
self.stdout.write(self.style.SUCCESS('✓ Email backend test successful'))
self.stdout.write(self.style.SUCCESS("✓ Email backend test successful"))
except Exception as e:
self.stdout.write(self.style.ERROR(f'✗ Email backend test failed: {str(e)}'))
self.stdout.write(
self.style.ERROR(
f"✗ Email backend test failed: {
str(e)}"
)
)
raise
def test_email_service_directly(self, to_email, site):
"""Test sending email directly via EmailService"""
self.stdout.write('\nTesting EmailService directly...')
self.stdout.write("\nTesting EmailService directly...")
try:
response = EmailService.send_email(
to=to_email,
subject='Test Email via Service',
text='This is a test email sent directly via the EmailService.',
site=site
subject="Test Email via Service",
text="This is a test email sent directly via the EmailService.",
site=site,
)
self.stdout.write(
self.style.SUCCESS("✓ Direct EmailService test successful")
)
self.stdout.write(self.style.SUCCESS('✓ Direct EmailService test successful'))
return response
except Exception as e:
self.stdout.write(self.style.ERROR(f'✗ Direct EmailService test failed: {str(e)}'))
self.stdout.write(
self.style.ERROR(
f"✗ Direct EmailService test failed: {
str(e)}"
)
)
raise

View File

@@ -43,7 +43,8 @@ class Migration(migrations.Migration):
(
"site",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="sites.site"
on_delete=django.db.models.deletion.CASCADE,
to="sites.site",
),
),
],
@@ -55,7 +56,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name="EmailConfigurationEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
(
"pgh_id",
models.AutoField(primary_key=True, serialize=False),
),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),

View File

@@ -3,11 +3,15 @@ from django.contrib.sites.models import Site
from core.history import TrackedModel
import pghistory
@pghistory.track()
class EmailConfiguration(TrackedModel):
api_key = models.CharField(max_length=255)
from_email = models.EmailField()
from_name = models.CharField(max_length=255, help_text="The name that will appear in the From field of emails")
from_name = models.CharField(
max_length=255,
help_text="The name that will appear in the From field of emails",
)
reply_to = models.EmailField()
site = models.ForeignKey(Site, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

View File

@@ -7,9 +7,20 @@ from .models import EmailConfiguration
import json
import base64
class EmailService:
@staticmethod
def send_email(*, to: str, subject: str, text: str, from_email: str = None, html: str = None, reply_to: str = None, request = None, site = None):
def send_email(
*,
to: str,
subject: str,
text: str,
from_email: str = None,
html: str = None,
reply_to: str = None,
request=None,
site=None,
):
# Get the site configuration
if site is None and request is not None:
site = get_current_site(request)
@@ -20,23 +31,28 @@ class EmailService:
# Fetch the email configuration for the current site
email_config = EmailConfiguration.objects.get(site=site)
api_key = email_config.api_key
# Use provided from_email or construct from config
if not from_email:
from_email = f"{email_config.from_name} <{email_config.from_email}>"
elif '<' not in from_email:
# If from_email is provided but doesn't include a name, add the configured name
from_email = f"{
email_config.from_name} <{
email_config.from_email}>"
elif "<" not in from_email:
# If from_email is provided but doesn't include a name, add the
# configured name
from_email = f"{email_config.from_name} <{from_email}>"
# Use provided reply_to or fall back to config
if not reply_to:
reply_to = email_config.reply_to
except EmailConfiguration.DoesNotExist:
raise ImproperlyConfigured(f"Email configuration is missing for site: {site.domain}")
raise ImproperlyConfigured(
f"Email configuration is missing for site: {site.domain}"
)
# Ensure the reply_to address is clean
reply_to = sanitize_address(reply_to, 'utf-8')
reply_to = sanitize_address(reply_to, "utf-8")
# Format data for the API
data = {
@@ -74,7 +90,8 @@ class EmailService:
f"{settings.FORWARD_EMAIL_BASE_URL}/v1/emails",
json=data,
headers=headers,
timeout=60)
timeout=60,
)
# Debug output
print(f"Response Status: {response.status_code}")
@@ -83,7 +100,10 @@ class EmailService:
if response.status_code != 200:
error_message = response.text if response.text else "Unknown error"
raise Exception(f"Failed to send email (Status {response.status_code}): {error_message}")
raise Exception(
f"Failed to send email (Status {
response.status_code}): {error_message}"
)
return response.json()
except requests.RequestException as e:

View File

@@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -2,5 +2,5 @@ from django.urls import path
from .views import SendEmailView
urlpatterns = [
path('send-email/', SendEmailView.as_view(), name='send-email'),
path("send-email/", SendEmailView.as_view(), name="send-email"),
]

View File

@@ -5,6 +5,7 @@ from rest_framework.permissions import AllowAny
from django.contrib.sites.shortcuts import get_current_site
from .services import EmailService
class SendEmailView(APIView):
permission_classes = [AllowAny] # Allow unauthenticated access
@@ -16,30 +17,33 @@ class SendEmailView(APIView):
from_email = data.get("from_email") # Optional
if not all([to, subject, text]):
return Response({
"error": "Missing required fields",
"required_fields": ["to", "subject", "text"]
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
"error": "Missing required fields",
"required_fields": ["to", "subject", "text"],
},
status=status.HTTP_400_BAD_REQUEST,
)
try:
# Get the current site
site = get_current_site(request)
# Send email using the site's configuration
response = EmailService.send_email(
to=to,
subject=subject,
text=text,
from_email=from_email, # Will use site's default if None
site=site
site=site,
)
return Response({
"message": "Email sent successfully",
"response": response
}, status=status.HTTP_200_OK)
return Response(
{"message": "Email sent successfully", "response": response},
status=status.HTTP_200_OK,
)
except Exception as e:
return Response({
"error": str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
{"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)