""" Email service for sending notifications related to agency messaging. """ from .models import Application from django.shortcuts import get_object_or_404 import logging from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.utils.html import strip_tags from django_q.tasks import async_task # Import needed at the top for clarity logger = logging.getLogger(__name__) from django.core.mail import send_mail, EmailMultiAlternatives from django.conf import settings from django.template.loader import render_to_string from django.utils.html import strip_tags from django.contrib.auth import get_user_model import logging from .models import Message logger = logging.getLogger(__name__) User = get_user_model() class EmailService: """ Legacy service class for handling email notifications. DEPRECATED: Use UnifiedEmailService from recruitment.services.email_service instead. """ def send_email(self, recipient_email, subject, body, html_body=None): """ DEPRECATED: Send email using unified email service. Args: recipient_email: Email address to send to subject: Email subject body: Plain text email body html_body: HTML email body (optional) Returns: dict: Result with success status and error message if failed """ try: from .services.email_service import UnifiedEmailService from .dto.email_dto import EmailConfig # Create unified email service service = UnifiedEmailService() # Create email configuration config = EmailConfig( to_email=recipient_email, subject=subject, html_content=html_body or body, context={"message": body} if not html_body else {}, ) # Send email using unified service result = service.send_email(config) return { "success": result.success, "error": result.error_details if not result.success else None, } except Exception as e: error_msg = f"Failed to send email to {recipient_email}: {str(e)}" logger.error(error_msg) return {"success": False, "error": error_msg} def send_agency_welcome_email(agency, access_link=None): """ Send welcome email to a new agency with portal access information. Args: agency: HiringAgency instance access_link: AgencyAccessLink instance (optional) Returns: bool: True if email was sent successfully, False otherwise """ try: if not agency.email: logger.warning(f"No email found for agency {agency.id}") return False context = { "agency": agency, "access_link": access_link, "portal_url": getattr( settings, "AGENCY_PORTAL_URL", "https://kaauh.edu.sa/portal/" ), } # Render email templates html_message = render_to_string( "recruitment/emails/agency_welcome.html", context ) plain_message = strip_tags(html_message) # Send email send_mail( subject="Welcome to KAAUH Recruitment Portal", message=plain_message, from_email=getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@kaauh.edu.sa"), recipient_list=[agency.email], html_message=html_message, fail_silently=False, ) logger.info(f"Welcome email sent to agency {agency.email}") return True except Exception as e: logger.error(f"Failed to send agency welcome email: {str(e)}") return False def send_assignment_notification_email(assignment, message_type="created"): """ Send email notification about assignment changes. Args: assignment: AgencyJobAssignment instance message_type: Type of notification ('created', 'updated', 'deadline_extended') Returns: bool: True if email was sent successfully, False otherwise """ try: if not assignment.agency.email: logger.warning(f"No email found for agency {assignment.agency.id}") return False context = { "assignment": assignment, "agency": assignment.agency, "job": assignment.job, "message_type": message_type, "portal_url": getattr( settings, "AGENCY_PORTAL_URL", "https://kaauh.edu.sa/portal/" ), } # Render email templates html_message = render_to_string( "recruitment/emails/assignment_notification.html", context ) plain_message = strip_tags(html_message) # Determine subject based on message type subjects = { "created": f"New Job Assignment: {assignment.job.title}", "updated": f"Assignment Updated: {assignment.job.title}", "deadline_extended": f"Deadline Extended: {assignment.job.title}", } subject = subjects.get( message_type, f"Assignment Notification: {assignment.job.title}" ) # Send email send_mail( subject=subject, message=plain_message, from_email=getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@kaauh.edu.sa"), recipient_list=[assignment.agency.email], html_message=html_message, fail_silently=False, ) logger.info( f"Assignment notification email sent to {assignment.agency.email} for {message_type}" ) return True except Exception as e: logger.error(f"Failed to send assignment notification email: {str(e)}") return False def send_interview_invitation_email( candidate, job, meeting_details=None, recipient_list=None ): """ Send interview invitation email using unified email service. DEPRECATED: Use UnifiedEmailService directly for better functionality. Args: candidate: Candidate instance job: Job instance meeting_details: Dictionary with meeting information (optional) recipient_list: List of additional email addresses (optional) Returns: dict: Result with success status and error message if failed """ try: from .services.email_service import UnifiedEmailService from .dto.email_dto import EmailConfig, EmailTemplate, EmailPriority # Create unified email service service = UnifiedEmailService() # Prepare recipient list recipients = [] if hasattr(candidate, "hiring_source") and candidate.hiring_source == "Agency": try: recipients.append(candidate.hiring_agency.email) except: pass else: recipients.append(candidate.email) if recipient_list: recipients.extend(recipient_list) if not recipients: return {"success": False, "error": "No recipient email addresses provided"} # Build interview context using template manager context = service.template_manager.build_interview_context( candidate, job, meeting_details ) # Send to each recipient results = [] for recipient_email in recipients: config = EmailConfig( to_email=recipient_email, subject=service.template_manager.get_subject_line( EmailTemplate.INTERVIEW_INVITATION, context ), template_name=EmailTemplate.INTERVIEW_INVITATION.value, context=context, priority=EmailPriority.HIGH, ) result = service.send_email(config) results.append(result.success) success_count = sum(results) return { "success": success_count > 0, "recipients_count": success_count, "message": f"Interview invitation sent to {success_count} out of {len(recipients)} recipient(s)", } except Exception as e: error_msg = f"Failed to send interview invitation email: {str(e)}" logger.error(error_msg, exc_info=True) return {"success": False, "error": error_msg} def send_bulk_email( subject, message, recipient_list, request=None, attachments=None, async_task_=False, job=None, ): """ Send bulk email to multiple recipients with HTML support and attachments, supporting synchronous or asynchronous dispatch. """ # --- 1. Categorization and Custom Message Preparation (CORRECTED) --- agency_emails = [] pure_candidate_emails = [] candidate_through_agency_emails = [] if not recipient_list: return {"success": False, "error": "No recipients provided"} # This must contain (final_recipient_email, customized_message) for ALL sends customized_sends = [] # 1a. Classify Recipients and Prepare Custom Messages for email in recipient_list: email = email.strip().lower() try: candidate = get_object_or_404(Application, person__email=email) except Exception: logger.warning(f"Candidate not found for email: {email}") continue candidate_name = candidate.person.full_name # --- Candidate belongs to an agency (Final Recipient: Agency) --- if candidate.hiring_agency and candidate.hiring_agency.email: agency_email = candidate.hiring_agency.email agency_message = f"Hi, {candidate_name}" + "\n" + message # Add Agency email as the recipient with the custom message customized_sends.append((agency_email, agency_message)) agency_emails.append(agency_email) candidate_through_agency_emails.append( candidate.email ) # For sync block only # --- Pure Candidate (Final Recipient: Candidate) --- else: candidate_message = f"Hi, {candidate_name}" + "\n" + message # Add Candidate email as the recipient with the custom message customized_sends.append((email, candidate_message)) pure_candidate_emails.append(email) # For sync block only # Calculate total recipients based on the size of the final send list total_recipients = len(customized_sends) if total_recipients == 0: return {"success": False, "error": "No valid recipients found for sending."} # --- 2. Handle ASYNC Dispatch (FIXED: Single loop used) --- if async_task_: try: processed_attachments = attachments if attachments else [] task_ids = [] job_id=job.id sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None # Loop through ALL final customized sends task_id = async_task( "recruitment.tasks.send_bulk_email_task", subject, customized_sends, processed_attachments, sender_user_id, job_id, hook='recruitment.tasks.email_success_hook', ) task_ids.append(task_id) logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.") return { "success": True, "async": True, "task_ids": task_ids, "message": f"Emails queued for background sending to {len(task_ids)} recipient(s).", } except ImportError: logger.error( "Async execution requested, but django_q or required modules not found. Defaulting to sync." ) async_task_ = False except Exception as e: logger.error(f"Failed to queue async tasks: {str(e)}", exc_info=True) return {"success": False, "error": f"Failed to queue async tasks: {str(e)}"} else: # --- 3. Handle SYNCHRONOUS Send (No changes needed here, as it was fixed previously) --- try: # NOTE: The synchronous block below should also use the 'customized_sends' # list for consistency instead of rebuilding messages from 'pure_candidate_emails' # and 'agency_emails', but keeping your current logic structure to minimize changes. from_email = getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@kaauh.edu.sa") is_html = "<" in message and ">" in message successful_sends = 0 # Helper Function for Sync Send (as provided) def send_individual_email(recipient, body_message): # ... (Existing helper function logic) ... nonlocal successful_sends if is_html: plain_message = strip_tags(body_message) email_obj = EmailMultiAlternatives( subject=subject, body=plain_message, from_email=from_email, to=[recipient], ) email_obj.attach_alternative(body_message, "text/html") else: email_obj = EmailMultiAlternatives( subject=subject, body=body_message, from_email=from_email, to=[recipient], ) if attachments: for attachment in attachments: if hasattr(attachment, "read"): filename = getattr(attachment, "name", "attachment") content = attachment.read() content_type = getattr( attachment, "content_type", "application/octet-stream" ) email_obj.attach(filename, content, content_type) elif isinstance(attachment, tuple) and len(attachment) == 3: filename, content, content_type = attachment email_obj.attach(filename, content, content_type) try: email_obj.send(fail_silently=False) successful_sends += 1 except Exception as e: logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True) # Send Emails - Pure Candidates for email in pure_candidate_emails: candidate_name = ( Application.objects.filter(email=email).first().first_name ) candidate_message = f"Hi, {candidate_name}" + "\n" + message send_individual_email(email, candidate_message) # Send Emails - Agencies i = 0 for email in agency_emails: candidate_email = candidate_through_agency_emails[i] candidate_name = ( Application.objects.filter(email=candidate_email).first().first_name ) agency_message = f"Hi, {candidate_name}" + "\n" + message send_individual_email(email, agency_message) i += 1 logger.info( f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients." ) return { "success": True, "recipients_count": successful_sends, "message": f"Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.", } except Exception as e: error_msg = f"Failed to process bulk email send request: {str(e)}" logger.error(error_msg, exc_info=True) return {"success": False, "error": error_msg}