kaauh_ats/recruitment/email_service.py
2025-12-14 12:47:27 +03:00

454 lines
16 KiB
Python

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