257 lines
8.0 KiB
Python
257 lines
8.0 KiB
Python
"""
|
|
Email utilities for sending submission notifications.
|
|
"""
|
|
|
|
import signal
|
|
import threading
|
|
import logging
|
|
from django.core.mail import send_mail, EmailMultiAlternatives
|
|
from django.template.loader import render_to_string
|
|
from django.conf import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TimeoutError(Exception):
|
|
"""Custom timeout exception"""
|
|
|
|
pass
|
|
|
|
|
|
def timeout_handler(signum, frame):
|
|
"""Signal handler for timeout"""
|
|
raise TimeoutError("Operation timed out")
|
|
|
|
|
|
def send_email_with_timeout(msg, timeout_seconds=5):
|
|
"""
|
|
Send email with a timeout to prevent hanging.
|
|
Uses signal-based timeout on Unix, threading fallback otherwise.
|
|
|
|
Args:
|
|
msg: EmailMultiAlternatives instance
|
|
timeout_seconds: Maximum time to wait for email to send
|
|
|
|
Returns:
|
|
bool: True if sent successfully, False otherwise
|
|
"""
|
|
result = [None] # Use list to allow modification in nested function
|
|
exception = [None]
|
|
|
|
def send_email():
|
|
try:
|
|
msg.send()
|
|
result[0] = True
|
|
except Exception as e:
|
|
exception[0] = e
|
|
result[0] = False
|
|
|
|
# Try signal-based timeout first (Unix/Linux)
|
|
try:
|
|
signal.signal(signal.SIGALRM, timeout_handler)
|
|
signal.alarm(timeout_seconds)
|
|
try:
|
|
msg.send()
|
|
signal.alarm(0) # Cancel the alarm
|
|
return True
|
|
except TimeoutError:
|
|
logger.error(f"Email sending timed out after {timeout_seconds} seconds")
|
|
signal.alarm(0) # Cancel the alarm
|
|
return False
|
|
except Exception as e:
|
|
signal.alarm(0) # Cancel the alarm
|
|
logger.error(f"Error sending email: {e}", exc_info=True)
|
|
return False
|
|
except (AttributeError, ValueError, OSError):
|
|
# Windows doesn't support SIGALRM, or signal already in use
|
|
# Use threading-based timeout as fallback
|
|
email_thread = threading.Thread(target=send_email)
|
|
email_thread.daemon = True
|
|
email_thread.start()
|
|
email_thread.join(timeout=timeout_seconds)
|
|
|
|
if email_thread.is_alive():
|
|
logger.error(
|
|
f"Email sending timed out after {timeout_seconds} seconds (threading)"
|
|
)
|
|
return False
|
|
|
|
if exception[0]:
|
|
logger.error(f"Error sending email: {exception[0]}", exc_info=True)
|
|
return False
|
|
|
|
return result[0] if result[0] is not None else False
|
|
|
|
|
|
def get_file_urls_with_base_url(files):
|
|
"""
|
|
Get absolute URLs for files.
|
|
Constructs full URLs using ALLOWED_HOSTS from settings.
|
|
"""
|
|
# Get base URL from ALLOWED_HOSTS
|
|
allowed_hosts = getattr(settings, "ALLOWED_HOSTS", [])
|
|
if allowed_hosts and allowed_hosts[0] != "*":
|
|
# Use first allowed host
|
|
domain = allowed_hosts[0]
|
|
# Check if it's localhost or IP for HTTP
|
|
if (
|
|
domain.startswith("127.0.0.1")
|
|
or domain.startswith("localhost")
|
|
or domain.startswith("10.")
|
|
):
|
|
# In DEBUG mode, add port 8000 (default Django dev server)
|
|
if settings.DEBUG:
|
|
base_url = f"http://{domain}:8000"
|
|
else:
|
|
base_url = f"http://{domain}"
|
|
else:
|
|
base_url = f"https://{domain}"
|
|
else:
|
|
# Fallback: use localhost with port in DEBUG mode
|
|
if settings.DEBUG:
|
|
base_url = "http://127.0.0.1:8000"
|
|
else:
|
|
base_url = ""
|
|
|
|
file_urls = []
|
|
for file in files:
|
|
# In DEBUG mode, use MEDIA_URL (WhiteNoise serves it)
|
|
# In production, use /uploads/ (nginx serves it)
|
|
if settings.DEBUG:
|
|
# Use MEDIA_URL which is "media/"
|
|
media_url = settings.MEDIA_URL.rstrip("/")
|
|
file_path = f"/{media_url}/{file.path}"
|
|
else:
|
|
# Use /uploads/ for nginx
|
|
file_path = f"/uploads/{file.path}"
|
|
|
|
if base_url:
|
|
# Make it an absolute URL
|
|
file_url = base_url + file_path
|
|
else:
|
|
# Fallback: relative URL
|
|
file_url = file_path
|
|
|
|
file_urls.append(
|
|
{
|
|
"file": file,
|
|
"url": file_url,
|
|
}
|
|
)
|
|
return file_urls
|
|
|
|
|
|
def send_submission_emails(submission):
|
|
"""
|
|
Send emails for a quote submission.
|
|
Sends:
|
|
1. Notification to owner (OWNER_EMAIL)
|
|
2. Confirmation to submitter (submission.email)
|
|
|
|
Returns tuple (owner_sent, submitter_sent)
|
|
"""
|
|
files = submission.files.all()
|
|
file_urls = get_file_urls_with_base_url(files)
|
|
owner_email = getattr(settings, "OWNER_EMAIL", "")
|
|
|
|
owner_sent = False
|
|
submitter_sent = False
|
|
|
|
# Email 1: Notification to owner
|
|
if owner_email:
|
|
try:
|
|
subject = f"New Quote Request - Submission #{submission.id}"
|
|
html_message = render_to_string(
|
|
"emails/submission_notification_owner.html",
|
|
{
|
|
"submission": submission,
|
|
"files": files,
|
|
"file_urls": file_urls,
|
|
},
|
|
)
|
|
|
|
# Plain text fallback
|
|
text_message = f"""
|
|
New Quote Request Received!
|
|
|
|
Submitted by: {submission.email}
|
|
Submitted at: {submission.submitted_at.strftime('%B %d, %Y %I:%M %p')}
|
|
Submission ID: #{submission.id}
|
|
|
|
Description:
|
|
{submission.description or '(No description provided)'}
|
|
|
|
Files uploaded: {files.count()}
|
|
"""
|
|
for file_url_data in file_urls:
|
|
file = file_url_data["file"]
|
|
url = file_url_data["url"]
|
|
text_message += f"- {file.original_filename} ({file.file_size} bytes)\n"
|
|
text_message += f" Download: {url}\n"
|
|
|
|
msg = EmailMultiAlternatives(
|
|
subject=subject,
|
|
body=text_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
to=[owner_email],
|
|
)
|
|
msg.attach_alternative(html_message, "text/html")
|
|
owner_sent = send_email_with_timeout(msg, timeout_seconds=30)
|
|
if not owner_sent:
|
|
logger.error(f"Failed to send owner email to {owner_email}")
|
|
except Exception as e:
|
|
logger.error(f"Error sending owner email: {e}", exc_info=True)
|
|
owner_sent = False
|
|
|
|
# Email 2: Confirmation to submitter
|
|
try:
|
|
subject = "Quote Request Received - Sedutto Machineworks"
|
|
html_message = render_to_string(
|
|
"emails/submission_confirmation.html",
|
|
{
|
|
"submission": submission,
|
|
"files": files,
|
|
"file_urls": file_urls,
|
|
},
|
|
)
|
|
|
|
# Plain text fallback
|
|
text_message = f"""
|
|
Thank you for your quote request!
|
|
|
|
We have received your submission and will review it shortly.
|
|
|
|
Submission Details:
|
|
Submission ID: #{submission.id}
|
|
Submitted at: {submission.submitted_at.strftime('%B %d, %Y %I:%M %p')}
|
|
|
|
Your Notes:
|
|
{submission.description or '(No notes provided)'}
|
|
|
|
Files uploaded: {files.count()}
|
|
"""
|
|
for file_url_data in file_urls:
|
|
file = file_url_data["file"]
|
|
url = file_url_data["url"]
|
|
text_message += f"- {file.original_filename} ({file.file_size} bytes)\n"
|
|
text_message += f" Download: {url}\n"
|
|
|
|
text_message += f"\nWe will contact you at {submission.email} regarding your quote request.\n"
|
|
text_message += f"\nThis submission will be kept on file until {submission.expiration_time.strftime('%B %d, %Y')}."
|
|
|
|
msg = EmailMultiAlternatives(
|
|
subject=subject,
|
|
body=text_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
to=[submission.email],
|
|
)
|
|
msg.attach_alternative(html_message, "text/html")
|
|
submitter_sent = send_email_with_timeout(msg, timeout_seconds=30)
|
|
if not submitter_sent:
|
|
logger.error(f"Failed to send submitter email to {submission.email}")
|
|
except Exception as e:
|
|
logger.error(f"Error sending submitter email: {e}", exc_info=True)
|
|
submitter_sent = False
|
|
|
|
return (owner_sent, submitter_sent)
|