Email config, email template and more!

This commit is contained in:
KenwoodFox
2025-12-03 18:46:48 -05:00
parent 0183b03bc2
commit 2836d5954e
14 changed files with 781 additions and 36 deletions

View File

@@ -0,0 +1,52 @@
"""
Email configuration parser and utilities.
Parses EMAIL_CONFIG environment variable in format:
smtp://<user>@kitsunehosting.net:<password>@smtp.forwardemail.net:465
"""
import os
import re
from urllib.parse import urlparse, unquote
def parse_email_config(email_config_str):
"""
Parse EMAIL_CONFIG string into email settings.
Format: smtp://<user>@kitsunehosting.net:<password>@smtp.forwardemail.net:465
Returns dict with: user, password, host, port, from_email
"""
if not email_config_str:
return None
# Parse the URL
# Format: smtp://user@domain:password@host:port
# We need to handle the @ symbols carefully
match = re.match(r"smtp://([^:]+):([^@]+)@([^:]+):(\d+)", email_config_str)
if not match:
raise ValueError(f"Invalid EMAIL_CONFIG format: {email_config_str}")
user_with_domain = match.group(1)
password = unquote(match.group(2))
host = match.group(3)
port = int(match.group(4))
# Extract email address (user@domain)
from_email = user_with_domain
return {
"user": user_with_domain,
"password": password,
"host": host,
"port": port,
"from_email": from_email,
}
def get_email_config():
"""Get email configuration from environment."""
email_config_str = os.environ.get("EMAIL_CONFIG")
if not email_config_str:
return None
return parse_email_config(email_config_str)

View File

@@ -3,6 +3,8 @@ import time
from pathlib import Path
from django.http import StreamingHttpResponse, JsonResponse
from django.conf import settings
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
@@ -33,40 +35,83 @@ def submit_quote(request):
def process_quote():
try:
if not settings.DEBUG:
raise Exception("Not implemented")
# Step 1: Validate email
yield send_progress_update(1, "Validating email address...", 5)
# Step 1: Upload files
yield send_progress_update(1, "Uploading files...", 25)
# Files are already uploaded at this point, just acknowledge
uploaded_files = []
for file in files:
uploaded_files.append({"name": file.name, "size": file.size})
if not email:
raise ValueError("Email is required")
# Step 2: Save files
yield send_progress_update(2, "Saving files...", 50)
# Save files to disk/storage
saved_paths = []
for file in files:
# Create store directory if it doesn't exist
store_dir = Path(settings.MEDIA_ROOT) / "uploads"
store_dir.mkdir(parents=True, exist_ok=True)
try:
validate_email(email)
except ValidationError:
raise ValueError(f"Invalid email address: {email}")
# Save file
file_path = store_dir / file.name
with open(file_path, "wb") as f:
yield send_progress_update(1, "Email validated successfully", 10)
# Step 2: Create submission & upload files
from quotes.models import Submission, SubmissionFile
# Create a Submission entry
submission = Submission.objects.create(
email=email,
description=notes,
)
yield send_progress_update(2, "Created submission entry...", 15)
# Save each file and register in the model
total_files = len(files)
file_objs = []
for idx, file in enumerate(files, start=1):
# Use the original filename and save to MEDIA_ROOT/submission_<id>_filename
submission_dir = (
Path(settings.MEDIA_ROOT) / f"submission_{submission.id}"
)
submission_dir.mkdir(parents=True, exist_ok=True)
target_path = submission_dir / file.name
with open(target_path, "wb") as f:
for chunk in file.chunks():
f.write(chunk)
saved_paths.append(str(file_path))
# Step 3: Send emails
yield send_progress_update(3, "Sending emails 1/2...", 70)
time.sleep(0.5)
# TODO: Actually send emails
yield send_progress_update(4, "Sending emails 2/2...", 75)
time.sleep(0.5)
# TODO: Actually send emails
emails_sent = True # Placeholder
# Register file in SubmissionFile model
rel_path = (Path(f"submission_{submission.id}") / file.name).as_posix()
submission_file = SubmissionFile.objects.create(
submission=submission,
original_filename=file.name,
path=rel_path,
file_size=file.size,
)
file_objs.append(submission_file)
yield send_progress_update(
2,
f"Uploaded file {idx} of {total_files}: {file.name}",
15 + int((idx / total_files) * 30),
)
yield send_progress_update(3, "Files saved...", 50)
# Step 4: Send emails
from quotes.email_utils import send_submission_emails
yield send_progress_update(4, "Sending notification email...", 70)
owner_sent, submitter_sent = send_submission_emails(submission)
time.sleep(0.3)
yield send_progress_update(4, "Sending confirmation email...", 75)
time.sleep(0.3)
# Verify emails were sent
if not owner_sent and getattr(settings, "OWNER_EMAIL", ""):
yield send_progress_update(
"warning",
"Warning: Owner notification email may not have been sent",
75,
)
if not submitter_sent:
yield send_progress_update(
"warning", "Warning: Confirmation email may not have been sent", 75
)
# Complete
yield send_progress_update(

View File

@@ -28,6 +28,7 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"quotes",
]
MIDDLEWARE = [
@@ -63,12 +64,25 @@ WSGI_APPLICATION = "seduttomachineworks_project.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
# Use local/data directory in DEBUG mode, otherwise use /app/data
if DEBUG:
DATA_DIR = BASE_DIR / "local" / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": DATA_DIR / "db.sqlite3",
}
}
else:
DATA_DIR = Path("/app/data")
DATA_DIR.mkdir(parents=True, exist_ok=True)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": DATA_DIR / "db.sqlite3",
}
}
}
# Internationalization
@@ -76,7 +90,7 @@ DATABASES = {
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
TIME_ZONE = "America/New_York"
USE_I18N = True
@@ -94,7 +108,17 @@ STATICFILES_DIRS = [BASE_DIR / "static"]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
# Use local/data/uploads in DEBUG mode, otherwise use /app/data/uploads
if DEBUG:
DATA_DIR = BASE_DIR / "local" / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
MEDIA_ROOT = DATA_DIR / "uploads"
MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
else:
DATA_DIR = Path("/app/data")
DATA_DIR.mkdir(parents=True, exist_ok=True)
MEDIA_ROOT = DATA_DIR / "uploads"
MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
@@ -103,3 +127,29 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Allow iframe embedding (for the quoting upload box)
# Note: We'll handle this per-view using @xframe_options_exempt decorator
# Email configuration
EMAIL_CONFIG = None
try:
from .email_config import get_email_config
email_config = get_email_config()
if email_config:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = email_config["host"]
EMAIL_PORT = email_config["port"]
EMAIL_USE_SSL = True # Port 465 uses SSL
EMAIL_HOST_USER = email_config["user"]
EMAIL_HOST_PASSWORD = email_config["password"]
DEFAULT_FROM_EMAIL = email_config["from_email"]
SERVER_EMAIL = email_config["from_email"]
EMAIL_CONFIG = email_config
else:
# Fallback to console backend for development
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
except Exception as e:
# If email config fails, use console backend
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Owner email for notifications
OWNER_EMAIL = os.environ.get("OWNER_EMAIL", "")