""" Email service for sending notifications related to agency messaging. """ 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 import logging logger = logging.getLogger(__name__) 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} from .models import Candidate def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False): """ Send bulk email to multiple recipients with HTML support and attachments, supporting synchronous or asynchronous dispatch. """ # Define messages (Placeholders) all_candidate_emails = [] candidate_through_agency_emails=[] participant_emails = [] agency_emails = [] left_candidate_emails = [] if not recipient_list: return {'success': False, 'error': 'No recipients provided'} for email in recipient_list: email = email.strip().lower() # Clean input email if email: candidate = Candidate.objects.filter(email=email).first() if candidate: all_candidate_emails.append(email) else: participant_emails.append(email) for email in all_candidate_emails: candidate = Candidate.objects.filter(email=email).first() if candidate: if candidate.hiring_source == 'Agency' and hasattr(candidate, 'hiring_agency') and candidate.hiring_agency: agency = candidate.hiring_agency candidate_through_agency_emails.append(email) if agency and agency.email: agency_emails.append(agency.email) else: left_candidate_emails.append(email) else: left_candidate_emails.append(email) # Determine unique recipients unique_left_candidates = list(set(left_candidate_emails)) # Convert to list for async task unique_agencies = list(agency_emails) unique_participants = list(set(participant_emails)) total_recipients = len(unique_left_candidates) + len(unique_agencies) + len(unique_participants) if total_recipients == 0: return {'success': False, 'error': 'No valid email addresses found after categorization'} # --- 3. Handle ASYNC Dispatch --- if async_task_: try: from django_q.tasks import async_task # Simple, serializable attachment format assumed: list of (filename, content, content_type) tuples processed_attachments = attachments if attachments else [] task_ids = [] # Queue Left Candidates if unique_left_candidates: task_id = async_task( 'recruitment.tasks.send_bulk_email_task', subject, message, recipient_list, processed_attachments, # Pass serializable data hook='recruitment.tasks.email_success_hook' # Example hook ) task_ids.append(task_id) logger.info(f" email queued. ID: {task_id}") return { 'success': True, 'async': True, 'task_ids': task_ids, 'message': f'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 # Fallback to sync 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)}"} # --- 4. Handle SYNCHRONOUS Send (If async_task_=False or fallback) --- try: 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 def send_individual_email(recipient, body_message): 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]) # Attachment Logic 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: # FIX: Added the critical .send() call 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 for email in unique_left_candidates: candidate_name=Candidate.objects.filter(email=email).first().first_name candidate_message = f"Hi, {candidate_name}"+"\n"+message send_individual_email(email, candidate_message) i=0 for email in unique_agencies: candidate_name=Candidate.objects.filter(email=candidate_through_agency_emails[i]).first().first_name agency_message = f"Hi, {candidate_name}"+"\n"+message send_individual_email(email, agency_message) for email in unique_participants: participant_message = "Hello Participant! This is a general notification for you." send_individual_email(email, participant_message) 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}