|
|
|
|
@ -1,26 +1,43 @@
|
|
|
|
|
{% extends "layout/base.html" %}
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
/* Match Inbox popup sizing */
|
|
|
|
|
.modal-xxl { max-width: 98vw; }
|
|
|
|
|
@media (min-width: 1400px) { .modal-xxl { max-width: 1400px; } }
|
|
|
|
|
|
|
|
|
|
#msg_body_container_iframe { height: 55vh; }
|
|
|
|
|
#msg_objects_container { max-height: 25vh; overflow: auto; }
|
|
|
|
|
|
|
|
|
|
.filter-card .form-label { font-size: 0.85rem; }
|
|
|
|
|
.filter-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
gap: 0.75rem 1rem;
|
|
|
|
|
}
|
|
|
|
|
@media (max-width: 1199px) {
|
|
|
|
|
.filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
|
|
}
|
|
|
|
|
@media (max-width: 767px) {
|
|
|
|
|
.filter-grid { grid-template-columns: 1fr; }
|
|
|
|
|
}
|
|
|
|
|
.filter-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
{# Pager macro must be defined before it is used #}
|
|
|
|
|
{% macro pager(position, page, total_pages, has_prev, has_next, filter_params) -%}
|
|
|
|
|
{% macro pager(position, page, total_pages, has_prev, has_next, filters) -%}
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center my-2">
|
|
|
|
|
<div>
|
|
|
|
|
{% if has_prev %}
|
|
|
|
|
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('main.admin_all_mails', page=page-1, **filter_params) }}">Previous</a>
|
|
|
|
|
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('main.admin_all_mail_page', page=page-1, **filters) }}">Previous</a>
|
|
|
|
|
{% else %}
|
|
|
|
|
<button class="btn btn-outline-secondary btn-sm" disabled>Previous</button>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if has_next %}
|
|
|
|
|
<a class="btn btn-outline-secondary btn-sm ms-2" href="{{ url_for('main.admin_all_mails', page=page+1, **filter_params) }}">Next</a>
|
|
|
|
|
<a class="btn btn-outline-secondary btn-sm ms-2" href="{{ url_for('main.admin_all_mail_page', page=page+1, **filters) }}">Next</a>
|
|
|
|
|
{% else %}
|
|
|
|
|
<button class="btn btn-outline-secondary btn-sm ms-2" disabled>Next</button>
|
|
|
|
|
{% endif %}
|
|
|
|
|
@ -29,11 +46,13 @@
|
|
|
|
|
<div class="d-flex align-items-center">
|
|
|
|
|
<span class="me-2">Page {{ page }} of {{ total_pages }}</span>
|
|
|
|
|
<form method="get" class="d-flex align-items-center mb-0">
|
|
|
|
|
{% for k, v in filter_params.items() %}
|
|
|
|
|
{% if v %}
|
|
|
|
|
{# Keep filters while paging #}
|
|
|
|
|
{% for k, v in filters.items() %}
|
|
|
|
|
{% if v is not none and v != '' %}
|
|
|
|
|
<input type="hidden" name="{{ k }}" value="{{ v }}" />
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
|
|
|
|
<label for="page_{{ position }}" class="form-label me-1 mb-0">Go to:</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
@ -54,71 +73,78 @@
|
|
|
|
|
{% block content %}
|
|
|
|
|
<h2 class="mb-3">All Mail</h2>
|
|
|
|
|
|
|
|
|
|
<div class="card mb-3 filter-card">
|
|
|
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
|
|
|
<span>Search Filters</span>
|
|
|
|
|
<div class="d-flex gap-3">
|
|
|
|
|
<a class="small" href="{{ url_for('main.admin_all_mails') }}">Clear Filter Values</a>
|
|
|
|
|
<button class="btn btn-primary btn-sm" type="submit" form="mailFilterForm">Search</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card mb-3">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<form id="mailFilterForm" method="get" action="{{ url_for('main.admin_all_mails') }}">
|
|
|
|
|
<div class="row g-3">
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="from_q">From contains</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="text" id="from_q" name="from_q" value="{{ filter_params.from_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="subject_q">Subject contains</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="text" id="subject_q" name="subject_q" value="{{ filter_params.subject_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="backup_q">Backup contains</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="text" id="backup_q" name="backup_q" value="{{ filter_params.backup_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="type_q">Type contains</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="text" id="type_q" name="type_q" value="{{ filter_params.type_q }}" />
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
|
|
|
<h5 class="mb-0">Search Filters</h5>
|
|
|
|
|
<div class="filter-actions">
|
|
|
|
|
<a class="btn btn-link btn-sm" href="{{ url_for('main.admin_all_mail_page') }}">Clear Filter Values</a>
|
|
|
|
|
<button type="submit" form="allMailFiltersForm" class="btn btn-primary btn-sm">Search</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form id="allMailFiltersForm" method="get" class="mb-0">
|
|
|
|
|
<div class="filter-grid">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">From contains</label>
|
|
|
|
|
<input class="form-control" type="text" name="from_q" value="{{ filters.from_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="job_name_q">Job name contains</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="text" id="job_name_q" name="job_name_q" value="{{ filter_params.job_name_q }}" />
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Subject contains</label>
|
|
|
|
|
<input class="form-control" type="text" name="subject_q" value="{{ filters.subject_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="received_from">Received >=</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="datetime-local" id="received_from" name="received_from" value="{{ filter_params.received_from }}" />
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Job name contains</label>
|
|
|
|
|
<input class="form-control" type="text" name="job_name_q" value="{{ filters.job_name_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-lg-3">
|
|
|
|
|
<label class="form-label" for="received_to">Received <=</label>
|
|
|
|
|
<input class="form-control form-control-sm" type="datetime-local" id="received_to" name="received_to" value="{{ filter_params.received_to }}" />
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Backup contains</label>
|
|
|
|
|
<input class="form-control" type="text" name="backup_q" value="{{ filters.backup_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-lg-3 d-flex align-items-end">
|
|
|
|
|
<div class="form-check">
|
|
|
|
|
<input class="form-check-input" type="checkbox" id="only_unlinked" name="only_unlinked" value="1" {% if filter_params.only_unlinked %}checked{% endif %} />
|
|
|
|
|
<label class="form-check-label" for="only_unlinked">Only unlinked</label>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Type contains</label>
|
|
|
|
|
<input class="form-control" type="text" name="type_q" value="{{ filters.type_q }}" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Only unlinked</label>
|
|
|
|
|
<div class="form-check mt-1">
|
|
|
|
|
<input class="form-check-input" type="checkbox" id="only_unlinked" name="only_unlinked" value="1" {% if filters.only_unlinked %}checked{% endif %} />
|
|
|
|
|
<label class="form-check-label" for="only_unlinked">Show only unlinked</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Received >=</label>
|
|
|
|
|
<input class="form-control" type="datetime-local" name="received_from" value="{{ filters.received_from }}" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label class="form-label mb-1">Received <=</label>
|
|
|
|
|
<input class="form-control" type="datetime-local" name="received_to" value="{{ filters.received_to }}" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{ pager("top", page, total_pages, has_prev, has_next, filter_params) }}
|
|
|
|
|
{{ pager("top", page, total_pages, has_prev, has_next, filters) }}
|
|
|
|
|
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-sm table-hover align-middle" id="mailAuditTable">
|
|
|
|
|
<table class="table table-sm table-hover align-middle" id="allMailTable">
|
|
|
|
|
<thead class="table-light">
|
|
|
|
|
<tr>
|
|
|
|
|
<th scope="col">Received</th>
|
|
|
|
|
<th scope="col">Date / time</th>
|
|
|
|
|
<th scope="col">From</th>
|
|
|
|
|
<th scope="col">Subject</th>
|
|
|
|
|
<th scope="col">Backup</th>
|
|
|
|
|
<th scope="col">Type</th>
|
|
|
|
|
<th scope="col">Job name</th>
|
|
|
|
|
<th scope="col">Linked</th>
|
|
|
|
|
<th scope="col">Parsed</th>
|
|
|
|
|
<th scope="col">EML</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
@ -134,35 +160,35 @@
|
|
|
|
|
<td>{{ row.job_name }}</td>
|
|
|
|
|
<td>
|
|
|
|
|
{% if row.linked %}
|
|
|
|
|
<span class="badge bg-success">Linked</span>
|
|
|
|
|
<span class="badge text-bg-success">Linked</span>
|
|
|
|
|
{% else %}
|
|
|
|
|
<span class="badge bg-warning text-dark">Unlinked</span>
|
|
|
|
|
<span class="badge text-bg-warning">Unlinked</span>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
<td>{{ row.parsed_at }}</td>
|
|
|
|
|
<td>
|
|
|
|
|
{% if row.has_eml %}
|
|
|
|
|
<a class="eml-download" href="{{ url_for('main.inbox_message_eml', message_id=row.id) }}" onclick="event.stopPropagation();">EML</a>
|
|
|
|
|
<a class="eml-download" href="{{ url_for('main.inbox_message_eml', message_id=row.id) }}">EML</a>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
{% else %}
|
|
|
|
|
<tr>
|
|
|
|
|
<td colspan="9" class="text-center text-muted py-3">No messages found.</td>
|
|
|
|
|
<td colspan="8" class="text-center text-muted py-3">No messages found.</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{ pager("bottom", page, total_pages, has_prev, has_next, filter_params) }}
|
|
|
|
|
{{ pager("bottom", page, total_pages, has_prev, has_next, filters) }}
|
|
|
|
|
|
|
|
|
|
<div class="modal fade" id="mailMessageModal" tabindex="-1" aria-labelledby="mailMessageModalLabel" aria-hidden="true">
|
|
|
|
|
<!-- Inline popup modal for message details (reuses Inbox detail endpoint) -->
|
|
|
|
|
<div class="modal fade" id="allMailMessageModal" tabindex="-1" aria-labelledby="allMailMessageModalLabel" aria-hidden="true">
|
|
|
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable modal-xxl">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title" id="mailMessageModalLabel">Message details</h5>
|
|
|
|
|
<h5 class="modal-title" id="allMailMessageModalLabel">Message details</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
@ -184,9 +210,6 @@
|
|
|
|
|
<dt class="col-4">Overall</dt>
|
|
|
|
|
<dd class="col-8" id="msg_overall"></dd>
|
|
|
|
|
|
|
|
|
|
<dt class="col-4">Customer</dt>
|
|
|
|
|
<dd class="col-8" id="msg_customer"></dd>
|
|
|
|
|
|
|
|
|
|
<dt class="col-4">Received</dt>
|
|
|
|
|
<dd class="col-8" id="msg_received"></dd>
|
|
|
|
|
|
|
|
|
|
@ -210,6 +233,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
|
|
|
</div>
|
|
|
|
|
@ -219,83 +243,87 @@
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
var table = document.getElementById('mailAuditTable');
|
|
|
|
|
var modalEl = document.getElementById('mailMessageModal');
|
|
|
|
|
if (!table || !modalEl) return;
|
|
|
|
|
|
|
|
|
|
var modal = new bootstrap.Modal(modalEl);
|
|
|
|
|
|
|
|
|
|
function setText(id, value) {
|
|
|
|
|
var el = document.getElementById(id);
|
|
|
|
|
if (el) el.textContent = value || '';
|
|
|
|
|
function wrapMailHtml(html) {
|
|
|
|
|
html = html || "";
|
|
|
|
|
return (
|
|
|
|
|
"<!doctype html><html><head><meta charset=\"utf-8\">" +
|
|
|
|
|
"<base target=\"_blank\">" +
|
|
|
|
|
"</head><body style=\"margin:0; padding:8px;\">" +
|
|
|
|
|
html +
|
|
|
|
|
"</body></html>"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderObjects(objects) {
|
|
|
|
|
var container = document.getElementById('msg_objects_container');
|
|
|
|
|
var container = document.getElementById("msg_objects_container");
|
|
|
|
|
if (!container) return;
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (!objects || !objects.length) {
|
|
|
|
|
container.innerHTML = '<div class="text-muted">No objects stored.</div>';
|
|
|
|
|
container.innerHTML = "<p class=\"text-muted mb-0\">No objects parsed for this message.</p>";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tableHtml = '<div class="table-responsive"><table class="table table-sm table-hover align-middle">' +
|
|
|
|
|
'<thead class="table-light"><tr><th>Name</th><th>Type</th><th>Status</th><th>Error</th></tr></thead><tbody>';
|
|
|
|
|
|
|
|
|
|
var html = "<div class=\"table-responsive\"><table class=\"table table-sm table-bordered mb-0\">";
|
|
|
|
|
html += "<thead><tr><th>Object</th><th>Type</th><th>Status</th><th>Error</th></tr></thead><tbody>";
|
|
|
|
|
for (var i = 0; i < objects.length; i++) {
|
|
|
|
|
var o = objects[i] || {};
|
|
|
|
|
tableHtml += '<tr>' +
|
|
|
|
|
'<td>' + (o.name || '') + '</td>' +
|
|
|
|
|
'<td>' + (o.type || '') + '</td>' +
|
|
|
|
|
'<td>' + (o.status || '') + '</td>' +
|
|
|
|
|
'<td style="white-space: pre-wrap;">' + (o.error_message || '') + '</td>' +
|
|
|
|
|
'</tr>';
|
|
|
|
|
html += "<tr>";
|
|
|
|
|
html += "<td>" + (o.name || "") + "</td>";
|
|
|
|
|
html += "<td>" + (o.type || "") + "</td>";
|
|
|
|
|
html += "<td>" + (o.status || "") + "</td>";
|
|
|
|
|
html += "<td>" + (o.error_message || "") + "</td>";
|
|
|
|
|
html += "</tr>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tableHtml += '</tbody></table></div>';
|
|
|
|
|
container.innerHTML = tableHtml;
|
|
|
|
|
html += "</tbody></table></div>";
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setIframeHtml(html) {
|
|
|
|
|
var iframe = document.getElementById('msg_body_container_iframe');
|
|
|
|
|
if (!iframe) return;
|
|
|
|
|
iframe.srcdoc = html || '<p>No message content stored.</p>';
|
|
|
|
|
}
|
|
|
|
|
// Prevent row click when clicking EML
|
|
|
|
|
document.querySelectorAll('a.eml-download').forEach(function (a) {
|
|
|
|
|
a.addEventListener('click', function (ev) { ev.stopPropagation(); });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function openMessage(messageId) {
|
|
|
|
|
try {
|
|
|
|
|
var res = await fetch('{{ url_for('main.inbox_message_detail', message_id=0) }}'.replace('/0', '/' + messageId));
|
|
|
|
|
if (!res.ok) throw new Error('Failed to load message');
|
|
|
|
|
var data = await res.json();
|
|
|
|
|
if (!data || data.status !== 'ok') throw new Error('Invalid response');
|
|
|
|
|
var table = document.getElementById('allMailTable');
|
|
|
|
|
var modalEl = document.getElementById('allMailMessageModal');
|
|
|
|
|
if (!table || !modalEl) return;
|
|
|
|
|
var modal = new bootstrap.Modal(modalEl);
|
|
|
|
|
|
|
|
|
|
var meta = data.meta || {};
|
|
|
|
|
setText('msg_from', meta.from_address);
|
|
|
|
|
setText('msg_backup', meta.backup_software);
|
|
|
|
|
setText('msg_type', meta.backup_type);
|
|
|
|
|
setText('msg_job', meta.job_name);
|
|
|
|
|
setText('msg_overall', meta.overall_status);
|
|
|
|
|
setText('msg_customer', meta.customer_name);
|
|
|
|
|
setText('msg_received', meta.received_at);
|
|
|
|
|
setText('msg_parsed', meta.parsed_at);
|
|
|
|
|
setText('msg_overall_message', meta.overall_message);
|
|
|
|
|
|
|
|
|
|
setIframeHtml(data.body_html);
|
|
|
|
|
renderObjects(data.objects);
|
|
|
|
|
|
|
|
|
|
modal.show();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert('Unable to open message details.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table.addEventListener('click', function (e) {
|
|
|
|
|
var tr = e.target.closest('tr.mail-row');
|
|
|
|
|
// Use event delegation so it always works (even after future DOM changes)
|
|
|
|
|
table.addEventListener('click', function (ev) {
|
|
|
|
|
var tr = ev.target && ev.target.closest ? ev.target.closest('tr[data-message-id]') : null;
|
|
|
|
|
if (!tr) return;
|
|
|
|
|
var id = tr.getAttribute('data-message-id');
|
|
|
|
|
if (!id) return;
|
|
|
|
|
openMessage(id);
|
|
|
|
|
|
|
|
|
|
fetch("{{ url_for('main.inbox_message_detail', message_id=0) }}".replace("0", id))
|
|
|
|
|
.then(function (resp) {
|
|
|
|
|
if (!resp.ok) throw new Error('Failed to load message details');
|
|
|
|
|
return resp.json();
|
|
|
|
|
})
|
|
|
|
|
.then(function (data) {
|
|
|
|
|
if (!data || data.status !== 'ok') throw new Error('Unexpected response');
|
|
|
|
|
var meta = data.meta || {};
|
|
|
|
|
|
|
|
|
|
document.getElementById('allMailMessageModalLabel').textContent = meta.subject || 'Message details';
|
|
|
|
|
document.getElementById('msg_from').textContent = meta.from_address || '';
|
|
|
|
|
document.getElementById('msg_backup').textContent = meta.backup_software || '';
|
|
|
|
|
document.getElementById('msg_type').textContent = meta.backup_type || '';
|
|
|
|
|
document.getElementById('msg_job').textContent = meta.job_name || '';
|
|
|
|
|
document.getElementById('msg_overall').textContent = meta.overall_status || '';
|
|
|
|
|
document.getElementById('msg_overall_message').textContent = meta.overall_message || '';
|
|
|
|
|
document.getElementById('msg_received').textContent = meta.received_at || '';
|
|
|
|
|
document.getElementById('msg_parsed').textContent = meta.parsed_at || '';
|
|
|
|
|
|
|
|
|
|
var bodyFrame = document.getElementById('msg_body_container_iframe');
|
|
|
|
|
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || '');
|
|
|
|
|
|
|
|
|
|
renderObjects(data.objects || []);
|
|
|
|
|
modal.show();
|
|
|
|
|
})
|
|
|
|
|
.catch(function (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
alert('Failed to load message details.');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
|