SMTP and other email realted settings

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-08 06:45:30 +00:00
parent 3651ee9ed4
commit d4ecddba22
7 changed files with 499 additions and 5 deletions

View File

@@ -2,15 +2,22 @@
Email Service - Multi-provider email sending
Uses Resend for transactional emails with fallback to Django's send_mail.
Also supports direct SMTP configuration.
Supports template rendering and multiple email types.
Configuration stored in IntegrationProvider model (provider_id='resend')
and EmailSettings model for SMTP and provider selection.
"""
import hashlib
import hmac
import logging
import re
import smtplib
import time
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from typing import Optional, List, Dict, Any
from urllib.parse import urlencode
from django.core.mail import send_mail
@@ -162,9 +169,10 @@ class EmailService:
Unified email service supporting multiple providers.
Primary: Resend (for production transactional emails)
Alternative: SMTP (configurable via EmailSettings)
Fallback: Django's send_mail (uses EMAIL_BACKEND from settings)
Uses EmailSettings model for configuration (from_email, from_name, etc.)
Uses EmailSettings model for configuration (from_email, from_name, provider selection, SMTP settings)
"""
def __init__(self):
@@ -172,6 +180,8 @@ class EmailService:
self._resend_config = {}
self._brevo_configured = False
self._brevo_config = {}
self._smtp_configured = False
self._smtp_config = {}
self._email_settings = None
self._setup_providers()
@@ -193,6 +203,22 @@ class EmailService:
else:
logger.info("Resend provider not configured in IntegrationProvider")
# Setup SMTP from EmailSettings
if self._email_settings:
smtp_host = self._email_settings.smtp_host
if smtp_host:
self._smtp_config = {
'host': smtp_host,
'port': self._email_settings.smtp_port,
'username': self._email_settings.smtp_username,
'password': self._email_settings.smtp_password,
'use_tls': self._email_settings.smtp_use_tls,
'use_ssl': self._email_settings.smtp_use_ssl,
'timeout': self._email_settings.smtp_timeout,
}
self._smtp_configured = True
logger.info(f"SMTP email provider initialized: {smtp_host}:{self._email_settings.smtp_port}")
# Setup Brevo (future - for marketing emails)
brevo_provider = IntegrationProvider.get_provider('brevo')
if brevo_provider and brevo_provider.api_key:
@@ -200,6 +226,14 @@ class EmailService:
self._brevo_configured = True
logger.info("Brevo email provider initialized")
@property
def email_provider(self) -> str:
"""Get selected email provider from EmailSettings"""
settings_obj = self._get_settings()
if settings_obj and settings_obj.email_provider:
return settings_obj.email_provider
return 'resend' # Default to resend
def _get_settings(self):
"""Get fresh email settings (refreshes on each call)"""
if not self._email_settings:
@@ -373,8 +407,32 @@ class EmailService:
sender_email = from_email or self.from_email
from_address = f"{sender_name} <{sender_email}>"
# Try Resend first
if self._resend_configured:
# Select email provider based on EmailSettings configuration
selected_provider = self.email_provider
if selected_provider == 'smtp' and self._smtp_configured:
return self._send_via_smtp(
to=to,
subject=subject,
html=html,
text=text,
from_address=from_address,
reply_to=reply_to or self.reply_to,
attachments=attachments,
)
elif selected_provider == 'resend' and self._resend_configured:
return self._send_via_resend(
to=to,
subject=subject,
html=html,
text=text,
from_address=from_address,
reply_to=reply_to or self.reply_to,
attachments=attachments,
tags=tags,
)
elif self._resend_configured:
# Fallback to Resend if it's configured
return self._send_via_resend(
to=to,
subject=subject,
@@ -465,6 +523,101 @@ class EmailService:
from_email=from_address.split('<')[-1].rstrip('>'),
)
def _send_via_smtp(
self,
to: List[str],
subject: str,
html: Optional[str],
text: Optional[str],
from_address: str,
reply_to: Optional[str],
attachments: Optional[List[Dict]],
) -> Dict[str, Any]:
"""Send email via direct SMTP connection"""
try:
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = from_address
msg['To'] = ', '.join(to)
if reply_to:
msg['Reply-To'] = reply_to
# Add custom headers
msg['X-Mailer'] = 'IGNY8 Email Service'
# Attach text version
if text:
part1 = MIMEText(text, 'plain', 'utf-8')
msg.attach(part1)
# Attach HTML version
if html:
part2 = MIMEText(html, 'html', 'utf-8')
msg.attach(part2)
# Handle attachments
if attachments:
for attachment in attachments:
part = MIMEBase('application', 'octet-stream')
content = attachment.get('content', b'')
if isinstance(content, str):
content = content.encode('utf-8')
part.set_payload(content)
encoders.encode_base64(part)
filename = attachment.get('filename', 'attachment')
part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
msg.attach(part)
# Get SMTP configuration
smtp_host = self._smtp_config.get('host')
smtp_port = self._smtp_config.get('port', 587)
smtp_username = self._smtp_config.get('username')
smtp_password = self._smtp_config.get('password')
use_tls = self._smtp_config.get('use_tls', True)
use_ssl = self._smtp_config.get('use_ssl', False)
timeout = self._smtp_config.get('timeout', 30)
# Connect and send
if use_ssl:
server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=timeout)
else:
server = smtplib.SMTP(smtp_host, smtp_port, timeout=timeout)
if use_tls:
server.starttls()
if smtp_username and smtp_password:
server.login(smtp_username, smtp_password)
server.sendmail(
from_address.split('<')[-1].rstrip('>'),
to,
msg.as_string()
)
server.quit()
logger.info(f"Email sent via SMTP: {subject} to {to}")
return {
'success': True,
'id': None, # SMTP doesn't provide message IDs like Resend
'provider': 'smtp',
}
except Exception as e:
logger.error(f"Failed to send email via SMTP: {str(e)}")
# Fallback to Django mail
logger.info("Falling back to Django mail backend")
return self._send_via_django(
to=to,
subject=subject,
html=html,
text=text,
from_email=from_address.split('<')[-1].rstrip('>'),
)
def _send_via_django(
self,
to: List[str],

View File

@@ -27,6 +27,7 @@ class EmailSettingsAdmin(Igny8ModelAdmin):
list_display = [
'from_email',
'from_name',
'email_provider',
'reply_to_email',
'send_welcome_emails',
'send_billing_emails',
@@ -35,9 +36,26 @@ class EmailSettingsAdmin(Igny8ModelAdmin):
readonly_fields = ['updated_at']
fieldsets = (
('Email Provider', {
'fields': ('email_provider',),
'description': 'Select the active email service provider. Configure SMTP settings below if using SMTP.',
}),
('SMTP Configuration', {
'fields': (
'smtp_host',
'smtp_port',
'smtp_username',
'smtp_password',
'smtp_use_tls',
'smtp_use_ssl',
'smtp_timeout',
),
'description': 'SMTP server settings. Required when email_provider is set to SMTP.',
'classes': ('collapse',),
}),
('Sender Configuration', {
'fields': ('from_email', 'from_name', 'reply_to_email'),
'description': 'Default sender settings. Email address must be verified in Resend.',
'description': 'Default sender settings. Email address must be verified in Resend (if using Resend) or configured in SMTP server.',
}),
('Company Branding', {
'fields': ('company_name', 'company_address', 'logo_url'),
@@ -65,6 +83,114 @@ class EmailSettingsAdmin(Igny8ModelAdmin):
}),
)
change_form_template = 'admin/system/emailsettings/change_form.html'
def get_urls(self):
"""Add custom URL for test email"""
urls = super().get_urls()
custom_urls = [
path(
'test-email/',
self.admin_site.admin_view(self.test_email_view),
name='system_emailsettings_test_email'
),
path(
'send-test-email/',
self.admin_site.admin_view(self.send_test_email),
name='system_emailsettings_send_test'
),
]
return custom_urls + urls
def test_email_view(self, request):
"""Show test email form"""
settings = EmailSettings.get_settings()
context = {
**self.admin_site.each_context(request),
'title': 'Send Test Email',
'settings': settings,
'opts': self.model._meta,
'default_from_email': settings.from_email,
'default_to_email': request.user.email,
}
return render(request, 'admin/system/emailsettings/test_email.html', context)
def send_test_email(self, request):
"""Send test email to verify configuration"""
if request.method != 'POST':
return JsonResponse({'error': 'POST required'}, status=405)
from django.utils import timezone
from igny8_core.business.billing.services.email_service import EmailService
to_email = request.POST.get('to_email', request.user.email)
subject = request.POST.get('subject', 'IGNY8 Test Email')
# Create fresh EmailService instance to pick up latest settings
service = EmailService()
settings = EmailSettings.get_settings()
test_html = f"""
<html>
<body style="font-family: Arial, sans-serif; padding: 20px;">
<h1 style="color: #6366f1;">IGNY8 Email Test</h1>
<p>This is a test email to verify your email configuration.</p>
<h3>Configuration Details:</h3>
<ul>
<li><strong>Provider:</strong> {settings.email_provider.upper()}</li>
<li><strong>From:</strong> {settings.from_name} &lt;{settings.from_email}&gt;</li>
<li><strong>Reply-To:</strong> {settings.reply_to_email}</li>
<li><strong>Sent At:</strong> {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}</li>
</ul>
<p style="color: #22c55e; font-weight: bold;">
✓ If you received this email, your email configuration is working correctly!
</p>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #e5e7eb;">
<p style="font-size: 12px; color: #6b7280;">
This is an automated test email from IGNY8 Admin.
</p>
</body>
</html>
"""
try:
result = service.send_transactional(
to=to_email,
subject=subject,
html=test_html,
tags=['test', 'admin-test'],
)
if result.get('success'):
# Log the test email
EmailLog.objects.create(
message_id=result.get('id', ''),
to_email=to_email,
from_email=settings.from_email,
subject=subject,
template_name='admin_test',
status='sent',
provider=result.get('provider', settings.email_provider),
tags=['test', 'admin-test'],
)
messages.success(
request,
f'Test email sent successfully to {to_email} via {result.get("provider", "unknown").upper()}!'
)
else:
messages.error(request, f'Failed to send: {result.get("error", "Unknown error")}')
except Exception as e:
messages.error(request, f'Error sending test email: {str(e)}')
return redirect(reverse('admin:system_emailsettings_changelist'))
def has_add_permission(self, request):
"""Only allow one instance (singleton)"""
return not EmailSettings.objects.exists()

View File

@@ -16,6 +16,52 @@ class EmailSettings(models.Model):
These settings work alongside IntegrationProvider (resend) configuration.
"""
EMAIL_PROVIDER_CHOICES = [
('resend', 'Resend'),
('smtp', 'SMTP'),
]
# Email provider selection
email_provider = models.CharField(
max_length=20,
choices=EMAIL_PROVIDER_CHOICES,
default='resend',
help_text='Active email service provider'
)
# SMTP Configuration
smtp_host = models.CharField(
max_length=255,
blank=True,
help_text='SMTP server hostname (e.g., smtp.gmail.com)'
)
smtp_port = models.IntegerField(
default=587,
help_text='SMTP server port (587 for TLS, 465 for SSL, 25 for plain)'
)
smtp_username = models.CharField(
max_length=255,
blank=True,
help_text='SMTP authentication username'
)
smtp_password = models.CharField(
max_length=255,
blank=True,
help_text='SMTP authentication password'
)
smtp_use_tls = models.BooleanField(
default=True,
help_text='Use TLS encryption (recommended for port 587)'
)
smtp_use_ssl = models.BooleanField(
default=False,
help_text='Use SSL encryption (for port 465)'
)
smtp_timeout = models.IntegerField(
default=30,
help_text='SMTP connection timeout in seconds'
)
# Default sender settings
from_email = models.EmailField(
default='noreply@igny8.com',

View File

@@ -0,0 +1,53 @@
# Generated by Django 5.2.10 on 2026-01-08 06:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0020_add_email_models'),
]
operations = [
migrations.AddField(
model_name='emailsettings',
name='email_provider',
field=models.CharField(choices=[('resend', 'Resend'), ('smtp', 'SMTP')], default='resend', help_text='Active email service provider', max_length=20),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_host',
field=models.CharField(blank=True, help_text='SMTP server hostname (e.g., smtp.gmail.com)', max_length=255),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_password',
field=models.CharField(blank=True, help_text='SMTP authentication password', max_length=255),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_port',
field=models.IntegerField(default=587, help_text='SMTP server port (587 for TLS, 465 for SSL, 25 for plain)'),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_timeout',
field=models.IntegerField(default=30, help_text='SMTP connection timeout in seconds'),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_use_ssl',
field=models.BooleanField(default=False, help_text='Use SSL encryption (for port 465)'),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_use_tls',
field=models.BooleanField(default=True, help_text='Use TLS encryption (recommended for port 587)'),
),
migrations.AddField(
model_name='emailsettings',
name='smtp_username',
field=models.CharField(blank=True, help_text='SMTP authentication username', max_length=255),
),
]

View File

@@ -0,0 +1,15 @@
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block submit_buttons_bottom %}
<div class="submit-row">
<input type="submit" value="{% trans 'Save' %}" class="default" name="_save">
<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue">
<a href="{% url 'admin:system_emailsettings_test_email' %}"
class="button"
style="float: right; background: #3b82f6; color: white; padding: 10px 20px; text-decoration: none; border-radius: 6px; margin-left: 10px;">
📧 Send Test Email
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block content %}
<div style="max-width: 800px; margin: 0 auto; padding: 20px;">
<div style="background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h2 style="margin-top: 0; color: #1f2937;">Send Test Email</h2>
<div style="background: #f0f9ff; border-radius: 6px; padding: 16px; margin-bottom: 24px; border-left: 4px solid #3b82f6;">
<h4 style="margin: 0 0 12px 0; color: #1e40af;">Current Configuration</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<p style="margin: 0; color: #1e3a5f; font-size: 14px;">
<strong>Provider:</strong>
<span style="background: {% if settings.email_provider == 'resend' %}#dbeafe{% else %}#fef3c7{% endif %};
padding: 2px 8px; border-radius: 4px;">
{{ settings.email_provider|upper }}
</span>
</p>
<p style="margin: 0; color: #1e3a5f; font-size: 14px;">
<strong>From Email:</strong> {{ settings.from_email }}
</p>
<p style="margin: 0; color: #1e3a5f; font-size: 14px;">
<strong>From Name:</strong> {{ settings.from_name }}
</p>
<p style="margin: 0; color: #1e3a5f; font-size: 14px;">
<strong>Reply-To:</strong> {{ settings.reply_to_email }}
</p>
</div>
{% if settings.email_provider == 'smtp' %}
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #bfdbfe;">
<p style="margin: 0 0 4px 0; color: #1e3a5f; font-size: 14px;">
<strong>SMTP Server:</strong> {{ settings.smtp_host }}:{{ settings.smtp_port }}
</p>
<p style="margin: 0; color: #1e3a5f; font-size: 14px;">
<strong>Encryption:</strong>
{% if settings.smtp_use_ssl %}SSL{% elif settings.smtp_use_tls %}TLS{% else %}None{% endif %}
</p>
</div>
{% endif %}
</div>
<form method="post" action="{% url 'admin:system_emailsettings_send_test' %}">
{% csrf_token %}
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: 500; color: #374151; margin-bottom: 8px;">
Send Test Email To:
</label>
<input type="email" name="to_email" value="{{ default_to_email }}"
style="width: 100%; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;"
placeholder="recipient@example.com" required>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: 500; color: #374151; margin-bottom: 8px;">
Subject:
</label>
<input type="text" name="subject" value="IGNY8 Test Email"
style="width: 100%; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;">
</div>
<div style="display: flex; gap: 12px;">
<button type="submit"
style="background: #6366f1; color: white; padding: 12px 24px; border: none;
border-radius: 6px; font-weight: 500; cursor: pointer; font-size: 14px;">
📧 Send Test Email
</button>
<a href="{% url 'admin:system_emailsettings_changelist' %}"
style="background: #e5e7eb; color: #374151; padding: 12px 24px;
border-radius: 6px; font-weight: 500; text-decoration: none; font-size: 14px;">
Cancel
</a>
</div>
</form>
</div>
<div style="background: #f0fdf4; border-radius: 8px; padding: 16px; margin-top: 20px; border-left: 4px solid #22c55e;">
<h4 style="margin: 0 0 8px 0; color: #166534;">✅ What This Test Does</h4>
<ul style="margin: 0; padding-left: 20px; color: #15803d; font-size: 14px;">
<li>Sends a test email using your currently selected provider ({{ settings.email_provider|upper }})</li>
<li>Verifies that your email configuration is working correctly</li>
<li>Logs the email in Email Logs for tracking</li>
<li>Shows the provider used and configuration details in the email body</li>
</ul>
</div>
{% if settings.email_provider == 'smtp' and not settings.smtp_host %}
<div style="background: #fef2f2; border-radius: 8px; padding: 16px; margin-top: 20px; border-left: 4px solid #ef4444;">
<h4 style="margin: 0 0 8px 0; color: #991b1b;">⚠️ SMTP Not Configured</h4>
<p style="margin: 0; color: #b91c1c; font-size: 14px;">
You have selected SMTP as your email provider, but SMTP settings are not configured.
Please go back and configure your SMTP server settings, or switch to Resend.
</p>
</div>
{% endif %}
</div>
{% endblock %}