email send from interview detail page
This commit is contained in:
parent
d90a8d5048
commit
d0975094f6
@ -237,64 +237,61 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False,job=None):
|
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,
|
Send bulk email to multiple recipients with HTML support and attachments,
|
||||||
supporting synchronous or asynchronous dispatch.
|
supporting synchronous or asynchronous dispatch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
|
# --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
|
||||||
if not from_interview:
|
|
||||||
|
|
||||||
agency_emails = []
|
agency_emails = []
|
||||||
pure_candidate_emails = []
|
pure_candidate_emails = []
|
||||||
candidate_through_agency_emails = []
|
candidate_through_agency_emails = []
|
||||||
|
|
||||||
if not recipient_list:
|
if not recipient_list:
|
||||||
return {'success': False, 'error': 'No recipients provided'}
|
return {'success': False, 'error': 'No recipients provided'}
|
||||||
|
|
||||||
# This must contain (final_recipient_email, customized_message) for ALL sends
|
# This must contain (final_recipient_email, customized_message) for ALL sends
|
||||||
customized_sends = []
|
customized_sends = []
|
||||||
|
|
||||||
# 1a. Classify Recipients and Prepare Custom Messages
|
# 1a. Classify Recipients and Prepare Custom Messages
|
||||||
for email in recipient_list:
|
for email in recipient_list:
|
||||||
email = email.strip().lower()
|
email = email.strip().lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
candidate = get_object_or_404(Application, person__email=email)
|
candidate = get_object_or_404(Application, person__email=email)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"Candidate not found for email: {email}")
|
logger.warning(f"Candidate not found for email: {email}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
candidate_name = candidate.person.full_name
|
candidate_name = candidate.person.full_name
|
||||||
|
|
||||||
|
|
||||||
# --- Candidate belongs to an agency (Final Recipient: Agency) ---
|
# --- Candidate belongs to an agency (Final Recipient: Agency) ---
|
||||||
if candidate.hiring_agency and candidate.hiring_agency.email:
|
if candidate.hiring_agency and candidate.hiring_agency.email:
|
||||||
agency_email = candidate.hiring_agency.email
|
agency_email = candidate.hiring_agency.email
|
||||||
agency_message = f"Hi, {candidate_name}" + "\n" + message
|
agency_message = f"Hi, {candidate_name}" + "\n" + message
|
||||||
|
|
||||||
# Add Agency email as the recipient with the custom message
|
# Add Agency email as the recipient with the custom message
|
||||||
customized_sends.append((agency_email, agency_message))
|
customized_sends.append((agency_email, agency_message))
|
||||||
agency_emails.append(agency_email)
|
agency_emails.append(agency_email)
|
||||||
candidate_through_agency_emails.append(candidate.email) # For sync block only
|
candidate_through_agency_emails.append(candidate.email) # For sync block only
|
||||||
|
|
||||||
# --- Pure Candidate (Final Recipient: Candidate) ---
|
# --- Pure Candidate (Final Recipient: Candidate) ---
|
||||||
else:
|
else:
|
||||||
candidate_message = f"Hi, {candidate_name}" + "\n" + message
|
candidate_message = f"Hi, {candidate_name}" + "\n" + message
|
||||||
|
|
||||||
# Add Candidate email as the recipient with the custom message
|
# Add Candidate email as the recipient with the custom message
|
||||||
customized_sends.append((email, candidate_message))
|
customized_sends.append((email, candidate_message))
|
||||||
pure_candidate_emails.append(email) # For sync block only
|
pure_candidate_emails.append(email) # For sync block only
|
||||||
|
|
||||||
# Calculate total recipients based on the size of the final send list
|
# Calculate total recipients based on the size of the final send list
|
||||||
total_recipients = len(customized_sends)
|
total_recipients = len(customized_sends)
|
||||||
|
|
||||||
|
if total_recipients == 0:
|
||||||
|
return {'success': False, 'error': 'No valid recipients found for sending.'}
|
||||||
|
|
||||||
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) ---
|
# --- 2. Handle ASYNC Dispatch (FIXED: Single loop used) ---
|
||||||
@ -306,49 +303,30 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
|
|||||||
|
|
||||||
job_id=job.id
|
job_id=job.id
|
||||||
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None
|
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
|
# Loop through ALL final customized sends
|
||||||
|
|
||||||
|
|
||||||
task_id = async_task(
|
task_id = async_task(
|
||||||
'recruitment.tasks.send_bulk_email_task',
|
'recruitment.tasks.send_bulk_email_task',
|
||||||
subject,
|
subject,
|
||||||
customized_sends,
|
customized_sends,
|
||||||
processed_attachments,
|
processed_attachments,
|
||||||
sender_user_id,
|
sender_user_id,
|
||||||
job_id,
|
job_id,
|
||||||
hook='recruitment.tasks.email_success_hook',
|
hook='recruitment.tasks.email_success_hook',
|
||||||
|
|
||||||
)
|
)
|
||||||
task_ids.append(task_id)
|
task_ids.append(task_id)
|
||||||
|
|
||||||
logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.")
|
logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'async': True,
|
'async': True,
|
||||||
'task_ids': task_ids,
|
'task_ids': task_ids,
|
||||||
'message': f'Emails queued for background sending to {len(task_ids)} recipient(s).'
|
'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:
|
except ImportError:
|
||||||
@ -398,38 +376,29 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
|
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
if not from_interview:
|
|
||||||
# Send Emails - Pure Candidates
|
# Send Emails - Pure Candidates
|
||||||
for email in pure_candidate_emails:
|
for email in pure_candidate_emails:
|
||||||
candidate_name = Application.objects.filter(email=email).first().first_name
|
candidate_name = Application.objects.filter(email=email).first().first_name
|
||||||
candidate_message = f"Hi, {candidate_name}" + "\n" + message
|
candidate_message = f"Hi, {candidate_name}" + "\n" + message
|
||||||
send_individual_email(email, candidate_message)
|
send_individual_email(email, candidate_message)
|
||||||
|
|
||||||
# Send Emails - Agencies
|
# Send Emails - Agencies
|
||||||
i = 0
|
i = 0
|
||||||
for email in agency_emails:
|
for email in agency_emails:
|
||||||
candidate_email = candidate_through_agency_emails[i]
|
candidate_email = candidate_through_agency_emails[i]
|
||||||
candidate_name = Application.objects.filter(email=candidate_email).first().first_name
|
candidate_name = Application.objects.filter(email=candidate_email).first().first_name
|
||||||
agency_message = f"Hi, {candidate_name}" + "\n" + message
|
agency_message = f"Hi, {candidate_name}" + "\n" + message
|
||||||
send_individual_email(email, agency_message)
|
send_individual_email(email, agency_message)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
|
logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'recipients_count': successful_sends,
|
'recipients_count': successful_sends,
|
||||||
'message': f'Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.'
|
'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:
|
except Exception as e:
|
||||||
error_msg = f"Failed to process bulk email send request: {str(e)}"
|
error_msg = f"Failed to process bulk email send request: {str(e)}"
|
||||||
|
|||||||
@ -2036,12 +2036,11 @@ class SettingsForm(forms.ModelForm):
|
|||||||
|
|
||||||
class InterviewEmailForm(forms.Form):
|
class InterviewEmailForm(forms.Form):
|
||||||
"""Form for composing emails to participants about a candidate"""
|
"""Form for composing emails to participants about a candidate"""
|
||||||
to = forms.MultipleChoiceField(
|
to = forms.CharField(
|
||||||
widget=forms.CheckboxSelectMultiple(attrs={
|
|
||||||
'class': 'form-check'
|
label=_('To'), # Use a descriptive label
|
||||||
}),
|
required=True,
|
||||||
label=_('Select Candidates'), # Use a descriptive label
|
|
||||||
required=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
subject = forms.CharField(
|
subject = forms.CharField(
|
||||||
@ -2069,14 +2068,16 @@ class InterviewEmailForm(forms.Form):
|
|||||||
def __init__(self, job, application,schedule, *args, **kwargs):
|
def __init__(self, job, application,schedule, *args, **kwargs):
|
||||||
applicant=application.person.user
|
applicant=application.person.user
|
||||||
interview=schedule.interview
|
interview=schedule.interview
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
if application.hiring_agency:
|
if application.hiring_agency:
|
||||||
self.fields['to'].initial=application.hiring_agency.email
|
self.fields['to'].initial=application.hiring_agency.email
|
||||||
|
self.fields['to'].disabled= True
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.fields['to'].initial=application.person.email
|
self.fields['to'].initial=application.person.email
|
||||||
|
self.fields['to'].disabled= True
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# Set initial message with candidate and meeting info
|
# Set initial message with candidate and meeting info
|
||||||
@ -2085,20 +2086,21 @@ Dear {applicant.first_name} {applicant.last_name},
|
|||||||
|
|
||||||
Your interview details are as follows:
|
Your interview details are as follows:
|
||||||
|
|
||||||
Date: {interview.interview_date}
|
Date: {interview.start_time.strftime("%d-%m-%Y")}
|
||||||
Time: {interview.interview_time}
|
Time: {interview.start_time.strftime("%I:%M %p")}
|
||||||
|
Interview Duration: {interview.duration} minutes
|
||||||
Job: {job.title}
|
Job: {job.title}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if schedule.location_type == 'Remote':
|
if interview.location_type == 'Remote':
|
||||||
initial_message += "This is a remote schedule. You will receive the meeting link separately.\n\n"
|
initial_message += f"Pease join using meeting link {interview.details_url} .\n\n"
|
||||||
else:
|
else:
|
||||||
email_body += "This is an onsite schedule. Please arrive 10 minutes early.\n\n"
|
initial_message += "This is an onsite schedule. Please arrive 10 minutes early.\n\n"
|
||||||
|
|
||||||
initial_message += """
|
initial_message += """
|
||||||
Best regards,
|
Best regards,
|
||||||
HR Team
|
KAAUH Hiring Team
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.fields['message'].initial = initial_message
|
self.fields['message'].initial = initial_message
|
||||||
|
|||||||
@ -90,7 +90,8 @@ from .forms import (
|
|||||||
BulkInterviewTemplateForm,
|
BulkInterviewTemplateForm,
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
InterviewCancelForm,
|
InterviewCancelForm,
|
||||||
InterviewEmailForm
|
InterviewEmailForm,
|
||||||
|
ApplicationStageForm
|
||||||
)
|
)
|
||||||
from .utils import generate_random_password
|
from .utils import generate_random_password
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
@ -1018,13 +1019,15 @@ def delete_form_template(request, template_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
# @login_required
|
||||||
@staff_or_candidate_required
|
# @staff_or_candidate_required
|
||||||
def application_submit_form(request, template_slug):
|
def application_submit_form(request, slug):
|
||||||
"""Display the form as a step-by-step wizard"""
|
"""Display the form as a step-by-step wizard"""
|
||||||
|
form_template=get_object_or_404(FormTemplate,slug=slug,is_active=True)
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return redirect("application_signup",slug=slug)
|
return redirect("application_signup",slug=slug)
|
||||||
job = get_object_or_404(JobPosting, form_template__slug=slug)
|
print(form_template.job.slug)
|
||||||
|
job = get_object_or_404(JobPosting, slug=form_template.job.slug)
|
||||||
if request.user.user_type == "candidate":
|
if request.user.user_type == "candidate":
|
||||||
person=request.user.person_profile
|
person=request.user.person_profile
|
||||||
if job.has_already_applied_to_this_job(person):
|
if job.has_already_applied_to_this_job(person):
|
||||||
@ -3949,11 +3952,11 @@ def api_application_detail(request, candidate_id):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def compose_application_email(request, job_slug):
|
def compose_application_email(request, slug):
|
||||||
"""Compose email to participants about a candidate"""
|
"""Compose email to participants about a candidate"""
|
||||||
from .email_service import send_bulk_email
|
from .email_service import send_bulk_email
|
||||||
|
|
||||||
job = get_object_or_404(JobPosting, slug=job_slug)
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
candidate_ids=request.GET.getlist('candidate_ids')
|
candidate_ids=request.GET.getlist('candidate_ids')
|
||||||
candidates=Application.objects.filter(id__in=candidate_ids)
|
candidates=Application.objects.filter(id__in=candidate_ids)
|
||||||
|
|
||||||
@ -3997,7 +4000,6 @@ def compose_application_email(request, job_slug):
|
|||||||
request=request,
|
request=request,
|
||||||
attachments=None,
|
attachments=None,
|
||||||
async_task_=True, # Changed to False to avoid pickle issues
|
async_task_=True, # Changed to False to avoid pickle issues
|
||||||
from_interview=False,
|
|
||||||
job=job
|
job=job
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -4386,16 +4388,19 @@ def interview_detail(request, slug):
|
|||||||
|
|
||||||
schedule = get_object_or_404(ScheduledInterview, slug=slug)
|
schedule = get_object_or_404(ScheduledInterview, slug=slug)
|
||||||
interview = schedule.interview
|
interview = schedule.interview
|
||||||
|
application=schedule.application
|
||||||
|
job=schedule.job
|
||||||
reschedule_form = ScheduledInterviewForm()
|
reschedule_form = ScheduledInterviewForm()
|
||||||
reschedule_form.initial['topic'] = interview.topic
|
reschedule_form.initial['topic'] = interview.topic
|
||||||
meeting=interview
|
meeting=interview
|
||||||
|
interview_email_form=InterviewEmailForm(job,application,schedule)
|
||||||
context = {
|
context = {
|
||||||
'schedule': schedule,
|
'schedule': schedule,
|
||||||
'interview': interview,
|
'interview': interview,
|
||||||
'reschedule_form':reschedule_form,
|
'reschedule_form':reschedule_form,
|
||||||
'interview_status_form':ScheduledInterviewUpdateStatusForm(),
|
'interview_status_form':ScheduledInterviewUpdateStatusForm(),
|
||||||
'cancel_form':InterviewCancelForm(instance=meeting),
|
'cancel_form':InterviewCancelForm(instance=meeting),
|
||||||
|
'interview_email_form':interview_email_form
|
||||||
}
|
}
|
||||||
return render(request, 'interviews/interview_detail.html', context)
|
return render(request, 'interviews/interview_detail.html', context)
|
||||||
|
|
||||||
@ -5741,17 +5746,40 @@ def sync_history(request, job_slug=None):
|
|||||||
|
|
||||||
def send_interview_email(request,slug):
|
def send_interview_email(request,slug):
|
||||||
schedule=get_object_or_404(ScheduledInterview,slug=slug)
|
schedule=get_object_or_404(ScheduledInterview,slug=slug)
|
||||||
application=schedule.application.first()
|
application=schedule.application
|
||||||
job=application.job
|
job=application.job
|
||||||
form=InterviewEmailForm(job,application,schedule)
|
form=InterviewEmailForm(job,application,schedule)
|
||||||
if request.method=='POST':
|
if request.method=='POST':
|
||||||
recipient=form.cleaned_data.get('to').strip()
|
form=InterviewEmailForm(job, application, schedule, request.POST)
|
||||||
body_message=form.cleaned_data.get('message')
|
if form.is_valid():
|
||||||
sender=request.user
|
recipient=form.cleaned_data.get('to').strip()
|
||||||
job=job
|
body_message=form.cleaned_data.get('message')
|
||||||
pass
|
subject=form.cleaned_data.get('subject')
|
||||||
|
sender=request.user
|
||||||
|
job=job
|
||||||
|
try:
|
||||||
|
email_result = async_task('recruitment.tasks._task_send_individual_email',
|
||||||
|
subject=subject,
|
||||||
|
body_message=body_message,
|
||||||
|
recipient=recipient,
|
||||||
|
attachments=None,
|
||||||
|
sender=sender,
|
||||||
|
job=job
|
||||||
|
)
|
||||||
|
if email_result:
|
||||||
|
messages.success(request, "Message sent successfully via email!")
|
||||||
|
else:
|
||||||
|
|
||||||
|
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
messages.warning(request, f"Message saved but email sending failed: {str(e)}")
|
||||||
|
else:
|
||||||
|
form=InterviewEmailForm(job,application,schedule)
|
||||||
|
else: # GET request
|
||||||
|
form = InterviewEmailForm(job, application, schedule)
|
||||||
|
|
||||||
|
# This is the final return, which handles GET requests and invalid POST requests.
|
||||||
|
return redirect('interview_detail',slug=schedule.slug)
|
||||||
# async_task('recruitment.tasks._task_send_individual_email', 'value1', 'value2')
|
|
||||||
# def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):
|
|
||||||
@ -595,38 +595,9 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form method="post" action="#">
|
<form method="post" action="{% url 'send_interview_email' schedule.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{interview_email_form|crispy}}
|
||||||
<label for="email_to" class="form-label">{% trans "To" %}</label>
|
|
||||||
<input type="email" class="form-control" id="email_to" value="{{ schedule.application.email }}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email_subject" class="form-label">{% trans "Subject" %}</label>
|
|
||||||
<input type="text" class="form-control" id="email_subject" name="subject"
|
|
||||||
value="{% trans 'Interview Details' %} - {{ schedule.job.title }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email_message" class="form-label">{% trans "Message" %}</label>
|
|
||||||
<textarea class="form-control" id="email_message" name="message" rows="6">
|
|
||||||
{% trans "Dear" %} {{ schedule.application.name }},
|
|
||||||
|
|
||||||
{% trans "Your interview details are as follows:" %}
|
|
||||||
|
|
||||||
{% trans "Date:" %} {{ schedule.interview_date|date:"d-m-Y" }}
|
|
||||||
{% trans "Time:" %} {{ schedule.interview_time|date:"h:i A" }}
|
|
||||||
{% trans "Job:" %} {{ schedule.job.title }}
|
|
||||||
|
|
||||||
{% if interview.location_type == 'Remote' %}
|
|
||||||
{% trans "This is a remote schedule. You will receive the meeting link separately." %}
|
|
||||||
{% else %}
|
|
||||||
{% trans "This is an onsite schedule. Please arrive 10 minutes early." %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% trans "Best regards," %}
|
|
||||||
{% trans "HR Team" %}
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %}
|
<i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user