""" 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)