Email config, email template and more!
This commit is contained in:
52
seduttomachineworks_project/email_config.py
Normal file
52
seduttomachineworks_project/email_config.py
Normal 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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
Reference in New Issue
Block a user