""" 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: """ Service class for handling email notifications """ def send_email(self, recipient_email, subject, body, html_body=None): """ Send email using Django's send_mail function 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: send_mail( subject=subject, message=body, from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'), recipient_list=[recipient_email], html_message=html_body, fail_silently=False, ) logger.info(f"Email sent successfully to {recipient_email}") return {'success': True} 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 HTML template. 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: # Prepare recipient list recipients = [] if 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'} # Prepare context for template context = { 'candidate_name': candidate.full_name or candidate.name, 'candidate_email': candidate.email, 'candidate_phone': candidate.phone or '', 'job_title': job.title, 'department': getattr(job, 'department', ''), 'company_name': getattr(settings, 'COMPANY_NAME', 'Norah University'), } # Add meeting details if provided if meeting_details: context.update({ 'meeting_topic': meeting_details.get('topic', f'Interview for {job.title}'), 'meeting_date_time': meeting_details.get('date_time', ''), 'meeting_duration': meeting_details.get('duration', '60 minutes'), 'join_url': meeting_details.get('join_url', ''), }) # Render HTML template html_message = render_to_string('emails/interview_invitation.html', context) plain_message = strip_tags(html_message) # Create email with both HTML and plain text versions email = EmailMultiAlternatives( subject=f'Interview Invitation: {job.title}', body=plain_message, from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'), to=recipients, ) email.attach_alternative(html_message, "text/html") # Send email email.send(fail_silently=False) logger.info(f"Interview invitation email sent successfully to {', '.join(recipients)}") return { 'success': True, 'recipients_count': len(recipients), 'message': f'Interview invitation sent successfully to {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, from_interview=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) --- if not from_interview: 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.'} else: # For interview flow total_recipients = len(recipient_list) # --- 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 if not from_interview: # Loop through ALL final customized sends for recipient_email, custom_message in customized_sends: task_id = async_task( 'recruitment.tasks.send_bulk_email_task', subject, custom_message, # Pass the custom message [recipient_email], # Pass the specific recipient as a list of one 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).' } else: # from_interview is True (generic send to all participants) task_id = async_task( 'recruitment.tasks.send_bulk_email_task', subject, message, recipient_list, # Send the original message to the entire list processed_attachments, hook='recruitment.tasks.email_success_hook' ) task_ids.append(task_id) logger.info(f"Interview emails queued. ID: {task_id}") return { 'success': True, 'async': True, 'task_ids': task_ids, 'message': f'Interview emails queued for background sending to {total_recipients} 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) if not from_interview: # 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.' } else: for email in recipient_list: send_individual_email(email, message) logger.info(f"Interview email processing complete. Sent successfully to {successful_sends} out of {total_recipients} recipients.") return { 'success': True, 'recipients_count': successful_sends, 'message': f'Interview emails sent successfully to {successful_sends} recipient(s).' } 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}