454 lines
16 KiB
Python
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}
|