mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:31:08 -05:00
first commit
This commit is contained in:
0
email_service/__init__.py
Normal file
0
email_service/__init__.py
Normal file
BIN
email_service/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
email_service/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
email_service/__pycache__/admin.cpython-311.pyc
Normal file
BIN
email_service/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
email_service/__pycache__/apps.cpython-311.pyc
Normal file
BIN
email_service/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
email_service/__pycache__/models.cpython-311.pyc
Normal file
BIN
email_service/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
email_service/__pycache__/services.cpython-311.pyc
Normal file
BIN
email_service/__pycache__/services.cpython-311.pyc
Normal file
Binary file not shown.
36
email_service/admin.py
Normal file
36
email_service/admin.py
Normal file
@@ -0,0 +1,36 @@
|
||||
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')
|
||||
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',)
|
||||
})
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
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')
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
6
email_service/apps.py
Normal file
6
email_service/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EmailServiceConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "email_service"
|
||||
91
email_service/backends.py
Normal file
91
email_service/backends.py
Normal file
@@ -0,0 +1,91 @@
|
||||
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)
|
||||
|
||||
def send_messages(self, email_messages):
|
||||
"""
|
||||
Send one or more EmailMessage objects and return the number of email
|
||||
messages sent.
|
||||
"""
|
||||
if not email_messages:
|
||||
return 0
|
||||
|
||||
num_sent = 0
|
||||
for message in email_messages:
|
||||
try:
|
||||
sent = self._send(message)
|
||||
if sent:
|
||||
num_sent += 1
|
||||
except Exception as e:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return num_sent
|
||||
|
||||
def _send(self, email_message):
|
||||
"""Send an EmailMessage object."""
|
||||
if not email_message.recipients():
|
||||
return False
|
||||
|
||||
# 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'):
|
||||
site = email_message.connection.site
|
||||
else:
|
||||
site = self.site
|
||||
|
||||
if not site:
|
||||
raise ValueError("Either request or site must be provided")
|
||||
|
||||
# Get the site's email configuration
|
||||
try:
|
||||
config = EmailConfiguration.objects.get(site=site)
|
||||
except EmailConfiguration.DoesNotExist:
|
||||
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)
|
||||
else:
|
||||
from_email = config.default_from_email
|
||||
|
||||
# Extract clean email address
|
||||
from_email = EmailService.extract_email(from_email)
|
||||
|
||||
# Get reply-to from message headers or use default
|
||||
reply_to = None
|
||||
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']
|
||||
|
||||
# Get message content
|
||||
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
|
||||
else:
|
||||
text = email_message.body
|
||||
|
||||
try:
|
||||
EmailService.send_email(
|
||||
to=to_email,
|
||||
subject=email_message.subject,
|
||||
text=text,
|
||||
from_email=from_email,
|
||||
reply_to=reply_to,
|
||||
site=site
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return False
|
||||
150
email_service/management/commands/test_email_flows.py
Normal file
150
email_service/management/commands/test_email_flows.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
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'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(enforce_csrf_checks=False) # Disable CSRF for testing
|
||||
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]"
|
||||
|
||||
# Add testserver to ALLOWED_HOSTS
|
||||
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')
|
||||
|
||||
# Clean up any existing test users
|
||||
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
|
||||
)
|
||||
EmailAddress.objects.create(
|
||||
user=user,
|
||||
email=user.email,
|
||||
primary=True,
|
||||
verified=True
|
||||
)
|
||||
|
||||
# Log in the test user
|
||||
self.client.force_login(user)
|
||||
|
||||
# Test other flows
|
||||
self.test_password_change(user)
|
||||
self.test_email_change(user)
|
||||
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'))
|
||||
|
||||
def test_registration(self):
|
||||
"""Test registration email flow"""
|
||||
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')
|
||||
|
||||
if response.status_code in [200, 201, 204]:
|
||||
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'
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
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...')
|
||||
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')
|
||||
|
||||
if response.status_code == 200:
|
||||
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'
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
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...')
|
||||
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')
|
||||
|
||||
if response.status_code == 200:
|
||||
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'
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
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...')
|
||||
try:
|
||||
# Request password reset
|
||||
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'))
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
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'))
|
||||
213
email_service/management/commands/test_email_service.py
Normal file
213
email_service/management/commands/test_email_service.py
Normal file
@@ -0,0 +1,213 @@
|
||||
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.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'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--to',
|
||||
type=str,
|
||||
help='Recipient email address (optional, defaults to current user\'s email)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key',
|
||||
type=str,
|
||||
help='ForwardEmail API key (optional, will use configured value)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--from-email',
|
||||
type=str,
|
||||
help='Sender email address (optional, will use configured value)',
|
||||
)
|
||||
|
||||
def get_config(self):
|
||||
"""Get email configuration from database or environment"""
|
||||
try:
|
||||
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
|
||||
}
|
||||
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')
|
||||
|
||||
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'
|
||||
))
|
||||
return None
|
||||
|
||||
return {
|
||||
'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...'))
|
||||
|
||||
# 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.'))
|
||||
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']
|
||||
|
||||
# If no recipient specified, use the from_email address for testing
|
||||
to_email = options['to'] or 'test@thrillwiki.com'
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'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}')
|
||||
|
||||
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)
|
||||
|
||||
# 3. Test API endpoint
|
||||
self.test_api_endpoint(to_email)
|
||||
|
||||
# 4. Test Django email backend
|
||||
self.test_email_backend(to_email, config.site)
|
||||
|
||||
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)}'))
|
||||
|
||||
def test_site_configuration(self, api_key, from_email):
|
||||
"""Test creating and retrieving 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'
|
||||
}
|
||||
)[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
|
||||
}
|
||||
)
|
||||
|
||||
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)}'))
|
||||
raise
|
||||
|
||||
def test_api_endpoint(self, to_email):
|
||||
"""Test sending email via the 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/',
|
||||
json={
|
||||
'to': to_email,
|
||||
'subject': 'Test Email via API',
|
||||
'text': 'This is a test email sent via the API endpoint.'
|
||||
},
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
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}'
|
||||
)
|
||||
)
|
||||
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.'
|
||||
)
|
||||
)
|
||||
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)}'))
|
||||
raise
|
||||
|
||||
def test_email_backend(self, to_email, site):
|
||||
"""Test sending email via Django's 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}')
|
||||
|
||||
send_mail(
|
||||
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
|
||||
)
|
||||
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)}'))
|
||||
raise
|
||||
|
||||
def test_email_service_directly(self, to_email, site):
|
||||
"""Test sending email directly via EmailService"""
|
||||
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
|
||||
)
|
||||
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)}'))
|
||||
raise
|
||||
33
email_service/migrations/0001_initial.py
Normal file
33
email_service/migrations/0001_initial.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-28 20:17
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('sites', '0002_alter_domain_unique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EmailConfiguration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('api_key', models.CharField(max_length=255)),
|
||||
('from_email', models.EmailField(max_length=254)),
|
||||
('from_name', models.CharField(help_text='The name that will appear in the From field of emails', max_length=255)),
|
||||
('reply_to', models.EmailField(max_length=254)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Email Configuration',
|
||||
'verbose_name_plural': 'Email Configurations',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
email_service/migrations/__init__.py
Normal file
0
email_service/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
email_service/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
email_service/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
18
email_service/models.py
Normal file
18
email_service/models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.db import models
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
class EmailConfiguration(models.Model):
|
||||
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")
|
||||
reply_to = models.EmailField()
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.from_name} <{self.from_email}>"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Email Configuration"
|
||||
verbose_name_plural = "Email Configurations"
|
||||
92
email_service/services.py
Normal file
92
email_service/services.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.mail.message import sanitize_address
|
||||
from .models import EmailConfiguration
|
||||
import json
|
||||
import base64
|
||||
|
||||
class EmailService:
|
||||
@staticmethod
|
||||
def send_email(to, subject, text, from_email=None, html=None, reply_to=None, request=None, site=None):
|
||||
# Get the site configuration
|
||||
if site is None and request is not None:
|
||||
site = get_current_site(request)
|
||||
elif site is None:
|
||||
raise ImproperlyConfigured("Either request or site must be provided")
|
||||
|
||||
try:
|
||||
# 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} <{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}")
|
||||
|
||||
# Ensure the reply_to address is clean
|
||||
reply_to = sanitize_address(reply_to, 'utf-8')
|
||||
|
||||
# Format data for the API
|
||||
data = {
|
||||
"from": from_email, # Now includes the name in format "Name <email@domain.com>"
|
||||
"to": to,
|
||||
"subject": subject,
|
||||
"text": text,
|
||||
"replyTo": reply_to,
|
||||
}
|
||||
|
||||
# Add HTML version if provided
|
||||
if html:
|
||||
data["html"] = html
|
||||
|
||||
# Debug output
|
||||
print("\nEmail Service Debug:")
|
||||
print(f"From: {from_email}")
|
||||
print(f"To: {to}")
|
||||
print(f"Reply-To: {reply_to}")
|
||||
print(f"API Key: {api_key}")
|
||||
print(f"Site: {site.domain}")
|
||||
print(f"Request URL: {settings.FORWARD_EMAIL_BASE_URL}/v1/emails")
|
||||
print(f"Request Data: {json.dumps(data, indent=2)}")
|
||||
|
||||
# Create Basic auth header with API key as username and empty password
|
||||
auth_header = base64.b64encode(f"{api_key}:".encode()).decode()
|
||||
headers = {
|
||||
"Authorization": f"Basic {auth_header}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{settings.FORWARD_EMAIL_BASE_URL}/v1/emails",
|
||||
json=data,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Debug output
|
||||
print(f"Response Status: {response.status_code}")
|
||||
print(f"Response Headers: {dict(response.headers)}")
|
||||
print(f"Response Body: {response.text}")
|
||||
|
||||
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}")
|
||||
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
raise Exception(f"Failed to send email: {str(e)}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to send email: {str(e)}")
|
||||
3
email_service/tests.py
Normal file
3
email_service/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
6
email_service/urls.py
Normal file
6
email_service/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import SendEmailView
|
||||
|
||||
urlpatterns = [
|
||||
path('send-email/', SendEmailView.as_view(), name='send-email'),
|
||||
]
|
||||
45
email_service/views.py
Normal file
45
email_service/views.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
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
|
||||
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
to = data.get("to")
|
||||
subject = data.get("subject")
|
||||
text = data.get("text")
|
||||
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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user