mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
7eddf9e85d
- Email::send(): add curl_error() check so transport failures (timeout, DNS, TLS) return a diagnosable error string instead of Unknown error - Email::send(): strip metadata key from options before array_merge so non-API fields are never sent to CyberMail endpoint - Email::send() + sendEmail(): include from-name in From field using RFC 5322 "Name <email>" format so fromName DB setting takes effect - email-log.php: replace unbounded page-link loop with a windowed paginator (first/last 2 pages + ±2 around current) with ellipsis gaps — prevents hundreds of anchors rendering at scale
249 lines
11 KiB
PHP
249 lines
11 KiB
PHP
<?php
|
|
ob_start();
|
|
/**
|
|
* Tom's Java Jive - Admin Email Log
|
|
*/
|
|
|
|
$pageTitle = 'Email Log';
|
|
require_once __DIR__ . '/includes/header.php';
|
|
|
|
// Refresh delivery status for a message
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'refresh_status') {
|
|
$logId = (int)($_POST['log_id'] ?? 0);
|
|
$messageId = trim($_POST['message_id'] ?? '');
|
|
if ($logId && $messageId) {
|
|
require_once dirname(__DIR__) . '/includes/email.php';
|
|
$data = emailService()->checkDeliveryStatus($messageId);
|
|
if (!empty($data)) {
|
|
$statusMap = ['queued'=>'sent','sent'=>'sent','delivered'=>'delivered','bounced'=>'bounced','failed'=>'failed'];
|
|
db()->update('email_log', [
|
|
'status' => $statusMap[$data['status']] ?? 'unknown',
|
|
'opened' => $data['opened'] ?? 0,
|
|
'opened_at' => !empty($data['opened_at']) ? date('Y-m-d H:i:s', strtotime($data['opened_at'])) : null,
|
|
'open_count' => $data['open_count'] ?? 0,
|
|
'clicked' => $data['clicked'] ?? 0,
|
|
'click_count' => $data['click_count'] ?? 0,
|
|
'status_checked_at'=> date('Y-m-d H:i:s'),
|
|
], 'id = :id', ['id' => $logId]);
|
|
}
|
|
}
|
|
header('Location: /admin/email-log.php' . (!empty($_POST['customer_filter']) ? '?customer=' . urlencode($_POST['customer_filter']) : ''));
|
|
exit;
|
|
}
|
|
|
|
// Filters
|
|
$customerFilter = trim($_GET['customer'] ?? '');
|
|
$statusFilter = trim($_GET['status'] ?? '');
|
|
$search = trim($_GET['search'] ?? '');
|
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
|
$perPage = 25;
|
|
$offset = ($page - 1) * $perPage;
|
|
|
|
$where = [];
|
|
$params = [];
|
|
|
|
if ($customerFilter) {
|
|
$where[] = 'l.customer_id = :customer_id';
|
|
$params['customer_id'] = $customerFilter;
|
|
}
|
|
if ($statusFilter) {
|
|
$where[] = 'l.status = :status';
|
|
$params['status'] = $statusFilter;
|
|
}
|
|
if ($search) {
|
|
$where[] = '(l.recipient_email LIKE :search OR l.subject LIKE :search)';
|
|
$params['search'] = '%' . $search . '%';
|
|
}
|
|
|
|
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
|
|
$total = db()->fetch(
|
|
"SELECT COUNT(*) as cnt FROM email_log l $whereClause",
|
|
$params
|
|
)['cnt'] ?? 0;
|
|
|
|
$logs = db()->fetchAll(
|
|
"SELECT l.*, c.name as customer_name
|
|
FROM email_log l
|
|
LEFT JOIN customers c ON l.customer_id = c.customer_id
|
|
$whereClause
|
|
ORDER BY l.sent_at DESC
|
|
LIMIT $perPage OFFSET $offset",
|
|
$params
|
|
);
|
|
|
|
$totalPages = max(1, ceil($total / $perPage));
|
|
|
|
// Status badge helper
|
|
$statusBadge = [
|
|
'sent' => ['bg'=>'#3B82F6','label'=>'Sent'],
|
|
'delivered' => ['bg'=>'#10B981','label'=>'Delivered'],
|
|
'bounced' => ['bg'=>'#EF4444','label'=>'Bounced'],
|
|
'failed' => ['bg'=>'#EF4444','label'=>'Failed'],
|
|
'unknown' => ['bg'=>'#9CA3AF','label'=>'Unknown'],
|
|
];
|
|
?>
|
|
|
|
<div class="admin-content">
|
|
<div class="content-header">
|
|
<h1 class="content-title"><i class="fas fa-envelope-open-text"></i> Email Log</h1>
|
|
<div style="display:flex;gap:10px;align-items:center;">
|
|
<span style="color:#666;font-size:.9em;"><?= number_format($total) ?> emails total</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px;align-items:flex-end;">
|
|
<div>
|
|
<label style="display:block;font-size:.8em;color:#666;margin-bottom:4px;">Search</label>
|
|
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>"
|
|
placeholder="Email or subject..." class="form-control" style="width:220px;">
|
|
</div>
|
|
<div>
|
|
<label style="display:block;font-size:.8em;color:#666;margin-bottom:4px;">Status</label>
|
|
<select name="status" class="form-control" style="width:140px;">
|
|
<option value="">All Statuses</option>
|
|
<?php foreach (array_keys($statusBadge) as $s): ?>
|
|
<option value="<?= $s ?>" <?= $statusFilter === $s ? 'selected' : '' ?>><?= ucfirst($s) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<?php if ($customerFilter): ?>
|
|
<input type="hidden" name="customer" value="<?= htmlspecialchars($customerFilter) ?>">
|
|
<div style="align-self:flex-end;">
|
|
<span style="background:#FF5E1A;color:white;padding:4px 10px;border-radius:4px;font-size:.85em;">
|
|
Filtered by customer
|
|
<a href="/admin/email-log.php" style="color:white;">×</a>
|
|
</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div style="align-self:flex-end;">
|
|
<button type="submit" class="btn btn-primary">Filter</button>
|
|
<a href="/admin/email-log.php" class="btn btn-secondary">Reset</a>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Table -->
|
|
<div class="table-container">
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Recipient</th>
|
|
<th>Subject</th>
|
|
<th>Preview</th>
|
|
<th>Status</th>
|
|
<th>Opened</th>
|
|
<th>Clicked</th>
|
|
<th>Sent At</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($logs)): ?>
|
|
<tr><td colspan="8" style="text-align:center;padding:40px;color:#999;">No emails found.</td></tr>
|
|
<?php else: foreach ($logs as $log):
|
|
$badge = $statusBadge[$log['status']] ?? $statusBadge['unknown'];
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<div><?= htmlspecialchars($log['recipient_email']) ?></div>
|
|
<?php if ($log['customer_name']): ?>
|
|
<small style="color:#666;">
|
|
<a href="/admin/customers.php?highlight=<?= urlencode($log['customer_id']) ?>" style="color:#FF5E1A;">
|
|
<?= htmlspecialchars($log['customer_name']) ?>
|
|
</a>
|
|
</small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
|
<?= htmlspecialchars($log['subject']) ?>
|
|
<?php if ($log['tags']): ?>
|
|
<br><small style="color:#9CA3AF;"><?= htmlspecialchars($log['tags']) ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#666;font-size:.85em;">
|
|
<?= htmlspecialchars($log['preview'] ?? '') ?>
|
|
</td>
|
|
<td>
|
|
<span style="background:<?= $badge['bg'] ?>;color:white;padding:3px 8px;border-radius:4px;font-size:.8em;font-weight:600;">
|
|
<?= $badge['label'] ?>
|
|
</span>
|
|
<?php if ($log['error_message']): ?>
|
|
<br><small style="color:#EF4444;font-size:.75em;"><?= htmlspecialchars(substr($log['error_message'],0,60)) ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="text-align:center;">
|
|
<?php if ($log['opened']): ?>
|
|
<span style="color:#10B981;" title="<?= $log['open_count'] ?> opens<?= $log['opened_at'] ? ', first '.date('M j g:ia',strtotime($log['opened_at'])) : '' ?>">
|
|
✓ <?= $log['open_count'] > 1 ? '('.$log['open_count'].')' : '' ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<span style="color:#9CA3AF;">—</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="text-align:center;">
|
|
<?php if ($log['clicked']): ?>
|
|
<span style="color:#3B82F6;">✓ <?= $log['click_count'] > 1 ? '('.$log['click_count'].')' : '' ?></span>
|
|
<?php else: ?>
|
|
<span style="color:#9CA3AF;">—</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="white-space:nowrap;font-size:.85em;">
|
|
<?= date('M j, Y', strtotime($log['sent_at'])) ?><br>
|
|
<span style="color:#666;"><?= date('g:i a', strtotime($log['sent_at'])) ?></span>
|
|
<?php if ($log['status_checked_at']): ?>
|
|
<br><small style="color:#9CA3AF;">checked <?= date('M j g:ia', strtotime($log['status_checked_at'])) ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if ($log['message_id']): ?>
|
|
<form method="POST" style="display:inline;">
|
|
<input type="hidden" name="action" value="refresh_status">
|
|
<input type="hidden" name="log_id" value="<?= $log['id'] ?>">
|
|
<input type="hidden" name="message_id" value="<?= htmlspecialchars($log['message_id']) ?>">
|
|
<?php if ($customerFilter): ?>
|
|
<input type="hidden" name="customer_filter" value="<?= htmlspecialchars($customerFilter) ?>">
|
|
<?php endif; ?>
|
|
<button type="submit" class="btn btn-sm btn-secondary" title="Refresh delivery status">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<?php if ($totalPages > 1):
|
|
$window = 2;
|
|
$pages = [];
|
|
for ($i = 1; $i <= $totalPages; $i++) {
|
|
if ($i <= $window || $i > $totalPages - $window || abs($i - $page) <= $window) {
|
|
$pages[] = $i;
|
|
}
|
|
}
|
|
$pages = array_unique($pages);
|
|
sort($pages);
|
|
$qs = '&search=' . urlencode($search) . '&status=' . urlencode($statusFilter) . '&customer=' . urlencode($customerFilter);
|
|
?>
|
|
<div style="display:flex;justify-content:center;gap:8px;margin-top:20px;flex-wrap:wrap;">
|
|
<?php $prev = null; foreach ($pages as $i):
|
|
if ($prev !== null && $i - $prev > 1): ?>
|
|
<span style="padding:6px 4px;color:#9CA3AF;">…</span>
|
|
<?php endif; ?>
|
|
<a href="?page=<?= $i ?><?= $qs ?>"
|
|
style="padding:6px 12px;border-radius:4px;text-decoration:none;
|
|
background:<?= $i === $page ? '#FF5E1A' : '#f3f4f6' ?>;
|
|
color:<?= $i === $page ? 'white' : '#374151' ?>;">
|
|
<?= $i ?>
|
|
</a>
|
|
<?php $prev = $i; endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|