mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
Switch email to CyberMail API; integrations page manages API key via DB
This commit is contained in:
@@ -0,0 +1,234 @@
|
|||||||
|
<?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): ?>
|
||||||
|
<div style="display:flex;justify-content:center;gap:8px;margin-top:20px;">
|
||||||
|
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||||
|
<a href="?page=<?= $i ?>&search=<?= urlencode($search) ?>&status=<?= urlencode($statusFilter) ?>&customer=<?= urlencode($customerFilter) ?>"
|
||||||
|
style="padding:6px 12px;border-radius:4px;text-decoration:none;
|
||||||
|
background:<?= $i === $page ? '#FF5E1A' : '#f3f4f6' ?>;
|
||||||
|
color:<?= $i === $page ? 'white' : '#374151' ?>;">
|
||||||
|
<?= $i ?>
|
||||||
|
</a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
+25
-27
@@ -3,20 +3,18 @@
|
|||||||
* Tom's Java Jive - Admin Integrations Settings
|
* Tom's Java Jive - Admin Integrations Settings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Auth only — no HTML yet
|
|
||||||
require_once __DIR__ . '/../includes/auth.php';
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
AdminAuth::require();
|
AdminAuth::require();
|
||||||
|
|
||||||
// Handle POST before any output
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$section = $_POST['section'] ?? '';
|
$section = $_POST['section'] ?? '';
|
||||||
|
|
||||||
if ($section === 'cybermail') {
|
if ($section === 'email') {
|
||||||
setSetting('cybermail_api_key', trim($_POST['cybermail_api_key'] ?? ''));
|
setSetting('cybermail_api_key', trim($_POST['cybermail_api_key'] ?? ''));
|
||||||
setSetting('cybermail_from_email', trim($_POST['cybermail_from_email'] ?? ''));
|
setSetting('cybermail_from_email', trim($_POST['cybermail_from_email'] ?? ''));
|
||||||
setSetting('cybermail_from_name', trim($_POST['cybermail_from_name'] ?? ''));
|
setSetting('cybermail_from_name', trim($_POST['cybermail_from_name'] ?? ''));
|
||||||
setSetting('email_notifications_enabled', isset($_POST['email_notifications_enabled']) ? '1' : '0');
|
setSetting('email_notifications_enabled', isset($_POST['email_notifications_enabled']) ? '1' : '0');
|
||||||
setFlash('success', 'CyberMail settings saved.');
|
setFlash('success', 'Email settings saved.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($section === 'twilio') {
|
if ($section === 'twilio') {
|
||||||
@@ -43,16 +41,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GET: render page ---
|
|
||||||
ob_start();
|
ob_start();
|
||||||
$pageTitle = 'Integrations';
|
$pageTitle = 'Integrations';
|
||||||
$currentPage = 'integrations';
|
$currentPage = 'integrations';
|
||||||
require_once __DIR__ . '/includes/header.php';
|
require_once __DIR__ . '/includes/header.php';
|
||||||
|
|
||||||
// Load current settings
|
$email = [
|
||||||
$cm = [
|
|
||||||
'api_key' => getSetting('cybermail_api_key', ''),
|
'api_key' => getSetting('cybermail_api_key', ''),
|
||||||
'from_email' => getSetting('cybermail_from_email', 'noreply@tomsjavajive.com'),
|
'from' => getSetting('cybermail_from_email', 'noreply@tomsjavajive.com'),
|
||||||
'from_name' => getSetting('cybermail_from_name', "Tom's Java Jive"),
|
'from_name' => getSetting('cybermail_from_name', "Tom's Java Jive"),
|
||||||
'enabled' => getSetting('email_notifications_enabled', '0'),
|
'enabled' => getSetting('email_notifications_enabled', '0'),
|
||||||
];
|
];
|
||||||
@@ -75,7 +71,7 @@ $loyaltyEnabled = getSetting('loyalty_enabled', '1') === '1';
|
|||||||
.integration-header { display: flex; justify-content: space-between; align-items: center; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--admin-border); }
|
.integration-header { display: flex; justify-content: space-between; align-items: center; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--admin-border); }
|
||||||
.integration-title { display: flex; align-items: center; gap: 1rem; }
|
.integration-title { display: flex; align-items: center; gap: 1rem; }
|
||||||
.integration-icon { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; }
|
.integration-icon { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; }
|
||||||
.integration-icon.cybermail { background: #0ea5e9; color: white; }
|
.integration-icon.email { background: #0ea5e9; color: white; }
|
||||||
.integration-icon.twilio { background: #F22F46; color: white; }
|
.integration-icon.twilio { background: #F22F46; color: white; }
|
||||||
.integration-icon.push { background: #8B5CF6; color: white; }
|
.integration-icon.push { background: #8B5CF6; color: white; }
|
||||||
.integration-icon.loyalty { background: #F59E0B; color: white; }
|
.integration-icon.loyalty { background: #F59E0B; color: white; }
|
||||||
@@ -99,49 +95,51 @@ $loyaltyEnabled = getSetting('loyalty_enabled', '1') === '1';
|
|||||||
<div class="alert alert-success mb-2"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
<div class="alert alert-success mb-2"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- CyberMail -->
|
<!-- Email — CyberMail -->
|
||||||
<div class="integration-card">
|
<div class="integration-card">
|
||||||
<div class="integration-header">
|
<div class="integration-header">
|
||||||
<div class="integration-title">
|
<div class="integration-title">
|
||||||
<div class="integration-icon cybermail"><i class="fas fa-envelope"></i></div>
|
<div class="integration-icon email"><i class="fas fa-envelope"></i></div>
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin:0;">CyberMail</h3>
|
<h3 style="margin:0;">Email — CyberMail</h3>
|
||||||
<p style="margin:.25rem 0 0;color:var(--admin-text-muted);font-size:.875rem;">Transactional email — order confirmations, shipping updates</p>
|
<p style="margin:.25rem 0 0;color:var(--admin-text-muted);font-size:.875rem;">Transactional email — order confirmations, shipping updates, password resets</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php $cmOk = !empty($cm['api_key']); $cmOn = $cm['enabled'] === '1'; ?>
|
<?php $emailOk = !empty($email['api_key']); $emailOn = $email['enabled'] === '1'; ?>
|
||||||
<span class="status-badge <?= $cmOk && $cmOn ? 'enabled' : ($cmOk ? 'configured' : 'not-configured') ?>">
|
<span class="status-badge <?= $emailOk && $emailOn ? 'enabled' : ($emailOk ? 'configured' : 'not-configured') ?>">
|
||||||
<?= $cmOk && $cmOn ? 'Enabled' : ($cmOk ? 'Configured' : 'Not Configured') ?>
|
<?= $emailOk && $emailOn ? 'Enabled' : ($emailOk ? 'Configured' : 'Not Configured') ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="integration-body">
|
<div class="integration-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="section" value="cybermail">
|
<input type="hidden" name="section" value="email">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">CyberMail API Key</label>
|
<label class="form-label">CyberMail API Key</label>
|
||||||
<input type="password" name="cybermail_api_key" class="form-input key-input"
|
<input type="password" name="cybermail_api_key" class="form-input key-input"
|
||||||
value="<?= htmlspecialchars($cm['api_key']) ?>" placeholder="sk_live_...">
|
value="<?= htmlspecialchars($email['api_key']) ?>" placeholder="sk_live_...">
|
||||||
<p class="help-text">Manage at <a href="https://platform.cyberpersons.com/email/api-keys/" target="_blank" class="help-link">CyberMail Dashboard</a></p>
|
<p class="help-text">Manage at <a href="https://platform.cyberpersons.com/email/api-keys/" target="_blank" class="help-link">CyberMail Dashboard</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">From Email</label>
|
<label class="form-label">From Email</label>
|
||||||
<input type="email" name="cybermail_from_email" class="form-input" value="<?= htmlspecialchars($cm['from_email']) ?>">
|
<input type="email" name="cybermail_from_email" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($email['from']) ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">From Name</label>
|
<label class="form-label">From Name</label>
|
||||||
<input type="text" name="cybermail_from_name" class="form-input" value="<?= htmlspecialchars($cm['from_name']) ?>">
|
<input type="text" name="cybermail_from_name" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($email['from_name']) ?>">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-checkbox">
|
<label class="form-checkbox">
|
||||||
<input type="checkbox" name="email_notifications_enabled" value="1" <?= $cmOn ? 'checked' : '' ?>>
|
<input type="checkbox" name="email_notifications_enabled" value="1" <?= $emailOn ? 'checked' : '' ?>>
|
||||||
Enable email notifications
|
Enable email notifications
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:.5rem;">
|
<div style="display:flex;gap:.5rem;">
|
||||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Save Settings</button>
|
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Save Settings</button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="testCyberMail()"><i class="fas fa-paper-plane"></i> Send Test Email</button>
|
<button type="button" class="btn btn-secondary" onclick="testEmail()"><i class="fas fa-paper-plane"></i> Send Test Email</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,7 +294,7 @@ $loyaltyEnabled = getSetting('loyalty_enabled', '1') === '1';
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let testType = '';
|
let testType = '';
|
||||||
function testCyberMail() {
|
function testEmail() {
|
||||||
testType = 'email';
|
testType = 'email';
|
||||||
document.getElementById('testModalTitle').textContent = 'Send Test Email';
|
document.getElementById('testModalTitle').textContent = 'Send Test Email';
|
||||||
document.getElementById('testInputLabel').textContent = 'Recipient Email';
|
document.getElementById('testInputLabel').textContent = 'Recipient Email';
|
||||||
@@ -322,19 +320,19 @@ document.getElementById('testForm').addEventListener('submit', async function(e)
|
|||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/test-notification.php', {
|
const r = await fetch('/api/test-notification.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({type: testType, recipient})
|
body: JSON.stringify({type: testType, recipient})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await r.json();
|
||||||
resultDiv.style.display = 'block';
|
resultDiv.style.display = 'block';
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
resultDiv.style.background = 'rgba(16,185,129,.1)';
|
resultDiv.style.background = 'rgba(16,185,129,.1)';
|
||||||
resultDiv.innerHTML = '<i class="fas fa-check-circle" style="color:var(--admin-success);"></i> ' + (data.message || 'Sent successfully!');
|
resultDiv.innerHTML = '<i class="fas fa-check-circle" style="color:var(--admin-success);"></i> ' + (data.message || 'Sent!');
|
||||||
} else {
|
} else {
|
||||||
resultDiv.style.background = 'rgba(239,68,68,.1)';
|
resultDiv.style.background = 'rgba(239,68,68,.1)';
|
||||||
resultDiv.innerHTML = '<i class="fas fa-times-circle" style="color:var(--admin-error);"></i> ' + (data.error || 'Failed to send');
|
resultDiv.innerHTML = '<i class="fas fa-times-circle" style="color:var(--admin-error);"></i> ' + (data.error || 'Failed');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resultDiv.style.display = 'block';
|
resultDiv.style.display = 'block';
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ switch ($type) {
|
|||||||
jsonResponse(['error' => 'Invalid email address'], 400);
|
jsonResponse(['error' => 'Invalid email address'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = sendEmail()->send(
|
$result = emailService()->send(
|
||||||
$recipient,
|
$recipient,
|
||||||
"Test Email from Tom's Java Jive",
|
"Test Email from Tom's Java Jive",
|
||||||
"<div style='font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px;'>
|
"<div style='font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px;'>
|
||||||
|
|||||||
+98
-151
@@ -1,77 +1,95 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - SendGrid Email Service
|
* Tom's Java Jive - CyberMail API Email Service
|
||||||
*
|
* Endpoint: POST https://platform.cyberpersons.com/email/v1/send
|
||||||
* Handles all transactional emails using SendGrid API
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class SendGridEmail {
|
class Email {
|
||||||
private string $apiKey;
|
private string $apiKey;
|
||||||
private string $fromEmail;
|
private string $fromEmail;
|
||||||
private string $fromName;
|
private string $fromName;
|
||||||
|
private string $endpoint = 'https://platform.cyberpersons.com/email/v1/send';
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
// Load from settings or config
|
$this->apiKey = getSetting('cybermail_api_key', defined('CYBERMAIL_API_KEY') ? CYBERMAIL_API_KEY : '');
|
||||||
$this->apiKey = getSetting('sendgrid_api_key', defined('SENDGRID_API_KEY') ? SENDGRID_API_KEY : '');
|
$this->fromEmail = defined('SENDER_EMAIL') ? SENDER_EMAIL : 'noreply@tomsjavajive.com';
|
||||||
$this->fromEmail = getSetting('sendgrid_from_email', defined('SENDER_EMAIL') ? SENDER_EMAIL : 'noreply@tomsjavajive.com');
|
$this->fromName = defined('SENDER_NAME') ? SENDER_NAME : "Tom's Java Jive";
|
||||||
$this->fromName = getSetting('sendgrid_from_name', defined('SENDER_NAME') ? SENDER_NAME : "Tom's Java Jive");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function logEmail(string $to, string $subject, string $html, array $result, array $options = []): void {
|
||||||
* Send email via SendGrid API
|
try {
|
||||||
*/
|
$preview = strip_tags($html);
|
||||||
public function send(string $to, string $subject, string $htmlContent, ?string $textContent = null): array {
|
$preview = preg_replace('/\s+/', ' ', trim($preview));
|
||||||
$data = [
|
$preview = substr($preview, 0, 500);
|
||||||
'personalizations' => [
|
|
||||||
[
|
// Find customer_id by email
|
||||||
|
$customer = db()->fetch(
|
||||||
|
"SELECT customer_id FROM customers WHERE email = :email LIMIT 1",
|
||||||
|
['email' => $to]
|
||||||
|
);
|
||||||
|
|
||||||
|
db()->insert('email_log', [
|
||||||
|
'customer_id' => $customer['customer_id'] ?? null,
|
||||||
|
'recipient_email'=> $to,
|
||||||
|
'subject' => $subject,
|
||||||
|
'preview' => $preview,
|
||||||
|
'message_id' => $result['message_id'] ?? null,
|
||||||
|
'status' => $result['success'] ? 'sent' : 'failed',
|
||||||
|
'error_message' => $result['success'] ? null : ($result['error'] ?? null),
|
||||||
|
'tags' => isset($options['tags']) ? implode(',', $options['tags']) : null,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Never let logging break email sending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkDeliveryStatus(string $messageId): array {
|
||||||
|
$ch = curl_init("https://platform.cyberpersons.com/email/v1/messages/" . urlencode($messageId));
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $this->apiKey],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
$body = json_decode($response, true);
|
||||||
|
return $body['data'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(string $to, string $subject, string $html, ?string $text = null, array $options = []): array {
|
||||||
|
$payload = array_merge([
|
||||||
|
'from' => ['email' => $this->fromEmail, 'name' => getSetting('cybermail_from_name', "Tom's Java Jive")],
|
||||||
'to' => [['email' => $to]],
|
'to' => [['email' => $to]],
|
||||||
'subject' => $subject
|
'subject' => $subject,
|
||||||
]
|
'html' => $html,
|
||||||
],
|
], $options);
|
||||||
'from' => [
|
if ($text) $payload['text'] = $text;
|
||||||
'email' => $this->fromEmail,
|
|
||||||
'name' => $this->fromName
|
|
||||||
],
|
|
||||||
'content' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($textContent) {
|
$ch = curl_init('https://platform.cyberpersons.com/email/v1/send');
|
||||||
$data['content'][] = ['type' => 'text/plain', 'value' => $textContent];
|
|
||||||
}
|
|
||||||
$data['content'][] = ['type' => 'text/html', 'value' => $htmlContent];
|
|
||||||
|
|
||||||
$ch = curl_init('https://api.sendgrid.com/v3/mail/send');
|
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_POST => true,
|
CURLOPT_POST => true,
|
||||||
CURLOPT_POSTFIELDS => json_encode($data),
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $this->apiKey, 'Content-Type: application/json'],
|
||||||
'Authorization: Bearer ' . $this->apiKey,
|
|
||||||
'Content-Type: application/json'
|
|
||||||
],
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_TIMEOUT => 30
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$error = curl_error($ch);
|
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($error) {
|
$body = json_decode($response, true);
|
||||||
return ['success' => false, 'error' => $error];
|
if ($httpCode === 202) {
|
||||||
|
$result = ['success' => true, 'message_id' => $body['data']['message_id'] ?? null];
|
||||||
|
$this->logEmail($to, $subject, $html, $result, $options);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$errorMsg = $body['error']['message'] ?? 'Unknown error';
|
||||||
|
$result = ['success' => false, 'error' => $errorMsg, 'code' => $httpCode];
|
||||||
|
$this->logEmail($to, $subject, $html, $result, $options);
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendGrid returns 202 for accepted
|
|
||||||
if ($httpCode >= 200 && $httpCode < 300) {
|
|
||||||
return ['success' => true];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => false, 'error' => $response, 'code' => $httpCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send order confirmation email
|
|
||||||
*/
|
|
||||||
public function sendOrderConfirmation(array $order): array {
|
public function sendOrderConfirmation(array $order): array {
|
||||||
$items = json_decode($order['items'], true);
|
$items = json_decode($order['items'], true);
|
||||||
$itemsHtml = '';
|
$itemsHtml = '';
|
||||||
@@ -85,7 +103,6 @@ class SendGridEmail {
|
|||||||
formatCurrency($item['total'])
|
formatCurrency($item['total'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = $this->getTemplate('order_confirmation', [
|
$html = $this->getTemplate('order_confirmation', [
|
||||||
'order_number' => $order['order_number'],
|
'order_number' => $order['order_number'],
|
||||||
'customer_name' => $order['customer_name'] ?? 'Valued Customer',
|
'customer_name' => $order['customer_name'] ?? 'Valued Customer',
|
||||||
@@ -97,17 +114,15 @@ class SendGridEmail {
|
|||||||
'payment_method' => ucfirst($order['payment_method'] ?? 'N/A'),
|
'payment_method' => ucfirst($order['payment_method'] ?? 'N/A'),
|
||||||
'order_date' => date('F j, Y', strtotime($order['created_at']))
|
'order_date' => date('F j, Y', strtotime($order['created_at']))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$order['customer_email'],
|
$order['customer_email'],
|
||||||
"Order Confirmation - #{$order['order_number']}",
|
"Order Confirmation - #{$order['order_number']}",
|
||||||
$html
|
$html,
|
||||||
|
null,
|
||||||
|
['tags' => ['order', 'confirmation'], 'metadata' => ['order_id' => $order['order_number']]]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send shipping notification email
|
|
||||||
*/
|
|
||||||
public function sendShippingNotification(array $order): array {
|
public function sendShippingNotification(array $order): array {
|
||||||
$html = $this->getTemplate('shipping_notification', [
|
$html = $this->getTemplate('shipping_notification', [
|
||||||
'order_number' => $order['order_number'],
|
'order_number' => $order['order_number'],
|
||||||
@@ -116,52 +131,33 @@ class SendGridEmail {
|
|||||||
'tracking_url' => $order['tracking_url'] ?? '#',
|
'tracking_url' => $order['tracking_url'] ?? '#',
|
||||||
'carrier' => $order['shipping_carrier'] ?? 'Our shipping partner'
|
'carrier' => $order['shipping_carrier'] ?? 'Our shipping partner'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->send(
|
return $this->send(
|
||||||
$order['customer_email'],
|
$order['customer_email'],
|
||||||
"Your Order Has Shipped - #{$order['order_number']}",
|
"Your Order Has Shipped - #{$order['order_number']}",
|
||||||
$html
|
$html,
|
||||||
|
null,
|
||||||
|
['tags' => ['order', 'shipping'], 'metadata' => ['order_id' => $order['order_number']]]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send password reset email
|
|
||||||
*/
|
|
||||||
public function sendPasswordReset(string $email, string $resetToken, string $name = ''): array {
|
public function sendPasswordReset(string $email, string $resetToken, string $name = ''): array {
|
||||||
$resetUrl = SITE_URL . '/reset-password.php?token=' . $resetToken;
|
$resetUrl = SITE_URL . '/reset-password.php?token=' . $resetToken;
|
||||||
|
|
||||||
$html = $this->getTemplate('password_reset', [
|
$html = $this->getTemplate('password_reset', [
|
||||||
'customer_name' => $name ?: 'there',
|
'customer_name' => $name ?: 'there',
|
||||||
'reset_url' => $resetUrl,
|
'reset_url' => $resetUrl,
|
||||||
'expires' => '1 hour'
|
'expires' => '1 hour'
|
||||||
]);
|
]);
|
||||||
|
return $this->send($email, "Reset Your Password - Tom's Java Jive", $html, null, ['tags' => ['password-reset']]);
|
||||||
return $this->send(
|
|
||||||
$email,
|
|
||||||
"Reset Your Password - Tom's Java Jive",
|
|
||||||
$html
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send welcome email to new customer
|
|
||||||
*/
|
|
||||||
public function sendWelcome(string $email, string $name = ''): array {
|
public function sendWelcome(string $email, string $name = ''): array {
|
||||||
$html = $this->getTemplate('welcome', [
|
$html = $this->getTemplate('welcome', [
|
||||||
'customer_name' => $name ?: 'Coffee Lover',
|
'customer_name' => $name ?: 'Coffee Lover',
|
||||||
'shop_url' => SITE_URL . '/shop.php'
|
'shop_url' => SITE_URL . '/shop.php'
|
||||||
]);
|
]);
|
||||||
|
return $this->send($email, "Welcome to Tom's Java Jive!", $html, null, ['tags' => ['welcome']]);
|
||||||
return $this->send(
|
|
||||||
$email,
|
|
||||||
"Welcome to Tom's Java Jive!",
|
|
||||||
$html
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send abandoned cart reminder
|
|
||||||
*/
|
|
||||||
public function sendAbandonedCartReminder(array $cart): array {
|
public function sendAbandonedCartReminder(array $cart): array {
|
||||||
$items = json_decode($cart['items'], true);
|
$items = json_decode($cart['items'], true);
|
||||||
$itemsHtml = '';
|
$itemsHtml = '';
|
||||||
@@ -172,24 +168,16 @@ class SendGridEmail {
|
|||||||
formatCurrency($item['price'])
|
formatCurrency($item['price'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = $this->getTemplate('abandoned_cart', [
|
$html = $this->getTemplate('abandoned_cart', [
|
||||||
'items_html' => $itemsHtml,
|
'items_html' => $itemsHtml,
|
||||||
'total' => formatCurrency($cart['subtotal']),
|
'total' => formatCurrency($cart['subtotal']),
|
||||||
'cart_url' => SITE_URL . '/cart.php'
|
'cart_url' => SITE_URL . '/cart.php'
|
||||||
]);
|
]);
|
||||||
|
return $this->send($cart['customer_email'], "You left something behind!", $html, null, ['tags' => ['abandoned-cart']]);
|
||||||
return $this->send(
|
|
||||||
$cart['customer_email'],
|
|
||||||
"You left something behind!",
|
|
||||||
$html
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get email template with variables replaced
|
|
||||||
*/
|
|
||||||
private function getTemplate(string $name, array $vars = []): string {
|
private function getTemplate(string $name, array $vars = []): string {
|
||||||
|
$year = date('Y');
|
||||||
$templates = [
|
$templates = [
|
||||||
'order_confirmation' => '
|
'order_confirmation' => '
|
||||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||||
@@ -199,43 +187,30 @@ class SendGridEmail {
|
|||||||
<div style="padding:30px;background:#fff;">
|
<div style="padding:30px;background:#fff;">
|
||||||
<p>Hi {{customer_name}},</p>
|
<p>Hi {{customer_name}},</p>
|
||||||
<p>Thank you for your order! We\'ve received it and will begin processing right away.</p>
|
<p>Thank you for your order! We\'ve received it and will begin processing right away.</p>
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:20px;border-radius:8px;margin:20px 0;">
|
<div style="background:#f9f9f9;padding:20px;border-radius:8px;margin:20px 0;">
|
||||||
<h3 style="margin-top:0;">Order #{{order_number}}</h3>
|
<h3 style="margin-top:0;">Order #{{order_number}}</h3>
|
||||||
<p style="color:#666;margin-bottom:0;">{{order_date}}</p>
|
<p style="color:#666;margin-bottom:0;">{{order_date}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table style="width:100%;border-collapse:collapse;">
|
<table style="width:100%;border-collapse:collapse;">
|
||||||
<thead>
|
<thead><tr style="background:#f5f5f5;">
|
||||||
<tr style="background: #f5f5f5;">
|
|
||||||
<th style="padding:10px;text-align:left;">Item</th>
|
<th style="padding:10px;text-align:left;">Item</th>
|
||||||
<th style="padding:10px;text-align:center;">Qty</th>
|
<th style="padding:10px;text-align:center;">Qty</th>
|
||||||
<th style="padding:10px;text-align:right;">Price</th>
|
<th style="padding:10px;text-align:right;">Price</th>
|
||||||
</tr>
|
</tr></thead>
|
||||||
</thead>
|
<tbody>{{items_html}}</tbody>
|
||||||
<tbody>
|
|
||||||
{{items_html}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div style="margin-top:20px;text-align:right;">
|
<div style="margin-top:20px;text-align:right;">
|
||||||
<p style="margin:5px 0;">Subtotal: {{subtotal}}</p>
|
<p style="margin:5px 0;">Subtotal: {{subtotal}}</p>
|
||||||
<p style="margin:5px 0;">Tax: {{tax}}</p>
|
<p style="margin:5px 0;">Tax: {{tax}}</p>
|
||||||
<p style="margin:5px 0;color:#10B981;">Discount: {{discount}}</p>
|
<p style="margin:5px 0;color:#10B981;">Discount: {{discount}}</p>
|
||||||
<p style="margin:5px 0;font-size:1.25em;font-weight:bold;color:#FF5E1A;">Total: {{total}}</p>
|
<p style="margin:5px 0;font-size:1.25em;font-weight:bold;color:#FF5E1A;">Total: {{total}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="margin-top:30px;">Payment Method: {{payment_method}}</p>
|
<p style="margin-top:30px;">Payment Method: {{payment_method}}</p>
|
||||||
|
|
||||||
<p style="color: #666; font-size: 0.9em; margin-top: 30px;">
|
|
||||||
If you have any questions, reply to this email or visit our website.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
||||||
<p style="margin: 0;">© ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
|
<p style="margin:0;">© ' . $year . ' Tom\'s Java Jive. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>',
|
||||||
',
|
|
||||||
|
|
||||||
'shipping_notification' => '
|
'shipping_notification' => '
|
||||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||||
@@ -244,27 +219,20 @@ class SendGridEmail {
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding:30px;background:#fff;">
|
<div style="padding:30px;background:#fff;">
|
||||||
<p>Hi {{customer_name}},</p>
|
<p>Hi {{customer_name}},</p>
|
||||||
<p>Great news! Your order #{{order_number}} is on its way to you.</p>
|
<p>Great news! Your order #{{order_number}} is on its way.</p>
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:20px;border-radius:8px;margin:20px 0;text-align:center;">
|
<div style="background:#f9f9f9;padding:20px;border-radius:8px;margin:20px 0;text-align:center;">
|
||||||
<p style="margin:0 0 10px;color:#666;">Tracking Number</p>
|
<p style="margin:0 0 10px;color:#666;">Tracking Number</p>
|
||||||
<p style="margin:0;font-size:1.25em;font-weight:bold;">{{tracking_number}}</p>
|
<p style="margin:0;font-size:1.25em;font-weight:bold;">{{tracking_number}}</p>
|
||||||
<p style="margin:10px 0 0;">Carrier: {{carrier}}</p>
|
<p style="margin:10px 0 0;">Carrier: {{carrier}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin:30px 0;">
|
<div style="text-align:center;margin:30px 0;">
|
||||||
<a href="{{tracking_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Track Your Package</a>
|
<a href="{{tracking_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Track Your Package</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color: #666; font-size: 0.9em;">
|
|
||||||
Please allow 24-48 hours for tracking information to update.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
||||||
<p style="margin: 0;">© ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
|
<p style="margin:0;">© ' . $year . ' Tom\'s Java Jive. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>',
|
||||||
',
|
|
||||||
|
|
||||||
'password_reset' => '
|
'password_reset' => '
|
||||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||||
@@ -273,21 +241,16 @@ class SendGridEmail {
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding:30px;background:#fff;">
|
<div style="padding:30px;background:#fff;">
|
||||||
<p>Hi {{customer_name}},</p>
|
<p>Hi {{customer_name}},</p>
|
||||||
<p>We received a request to reset your password. Click the button below to create a new password:</p>
|
<p>We received a request to reset your password. Click the button below to create a new one:</p>
|
||||||
|
|
||||||
<div style="text-align:center;margin:30px 0;">
|
<div style="text-align:center;margin:30px 0;">
|
||||||
<a href="{{reset_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Reset Password</a>
|
<a href="{{reset_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Reset Password</a>
|
||||||
</div>
|
</div>
|
||||||
|
<p style="color:#666;font-size:0.9em;">This link expires in {{expires}}. If you didn\'t request this, ignore this email.</p>
|
||||||
<p style="color: #666; font-size: 0.9em;">
|
|
||||||
This link will expire in {{expires}}. If you didn\'t request this, you can safely ignore this email.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
||||||
<p style="margin: 0;">© ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
|
<p style="margin:0;">© ' . $year . ' Tom\'s Java Jive. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>',
|
||||||
',
|
|
||||||
|
|
||||||
'welcome' => '
|
'welcome' => '
|
||||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||||
@@ -297,7 +260,6 @@ class SendGridEmail {
|
|||||||
<div style="padding:30px;background:#fff;">
|
<div style="padding:30px;background:#fff;">
|
||||||
<p>Hi {{customer_name}},</p>
|
<p>Hi {{customer_name}},</p>
|
||||||
<p>Welcome to Tom\'s Java Jive! We\'re thrilled to have you join our community of coffee lovers.</p>
|
<p>Welcome to Tom\'s Java Jive! We\'re thrilled to have you join our community of coffee lovers.</p>
|
||||||
|
|
||||||
<h3>Here\'s what you can look forward to:</h3>
|
<h3>Here\'s what you can look forward to:</h3>
|
||||||
<ul style="color:#666;">
|
<ul style="color:#666;">
|
||||||
<li>Exclusive member discounts</li>
|
<li>Exclusive member discounts</li>
|
||||||
@@ -305,18 +267,15 @@ class SendGridEmail {
|
|||||||
<li>Reward points on every purchase</li>
|
<li>Reward points on every purchase</li>
|
||||||
<li>Birthday treats and special offers</li>
|
<li>Birthday treats and special offers</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div style="text-align:center;margin:30px 0;">
|
<div style="text-align:center;margin:30px 0;">
|
||||||
<a href="{{shop_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Start Shopping</a>
|
<a href="{{shop_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Start Shopping</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Cheers,<br>The Tom\'s Java Jive Team</p>
|
<p>Cheers,<br>The Tom\'s Java Jive Team</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
||||||
<p style="margin: 0;">© ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
|
<p style="margin:0;">© ' . $year . ' Tom\'s Java Jive. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>',
|
||||||
',
|
|
||||||
|
|
||||||
'abandoned_cart' => '
|
'abandoned_cart' => '
|
||||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||||
@@ -326,44 +285,32 @@ class SendGridEmail {
|
|||||||
<div style="padding:30px;background:#fff;">
|
<div style="padding:30px;background:#fff;">
|
||||||
<p>Hey there!</p>
|
<p>Hey there!</p>
|
||||||
<p>We noticed you left some amazing items in your cart. Don\'t let them get away!</p>
|
<p>We noticed you left some amazing items in your cart. Don\'t let them get away!</p>
|
||||||
|
|
||||||
<div style="background:#f9f9f9;padding:20px;border-radius:8px;margin:20px 0;">
|
<div style="background:#f9f9f9;padding:20px;border-radius:8px;margin:20px 0;">
|
||||||
{{items_html}}
|
{{items_html}}
|
||||||
<div style="padding: 15px 10px; font-weight: bold; text-align: right;">
|
<div style="padding:15px 10px;font-weight:bold;text-align:right;">Total: {{total}}</div>
|
||||||
Total: {{total}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align:center;margin:30px 0;">
|
<div style="text-align:center;margin:30px 0;">
|
||||||
<a href="{{cart_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Complete Your Order</a>
|
<a href="{{cart_url}}" style="display:inline-block;background:#FF5E1A;color:white;padding:15px 30px;text-decoration:none;border-radius:8px;font-weight:bold;">Complete Your Order</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color: #666; font-size: 0.9em;">
|
|
||||||
Need help? Just reply to this email and we\'ll assist you!
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
<div style="background:#333;color:#fff;padding:20px;text-align:center;">
|
||||||
<p style="margin: 0;">© ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
|
<p style="margin:0;">© ' . $year . ' Tom\'s Java Jive. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>'
|
||||||
'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$template = $templates[$name] ?? '<p>Email template not found.</p>';
|
$template = $templates[$name] ?? '<p>Email template not found.</p>';
|
||||||
|
|
||||||
foreach ($vars as $key => $value) {
|
foreach ($vars as $key => $value) {
|
||||||
$template = str_replace('{{' . $key . '}}', $value, $template);
|
$template = str_replace('{{' . $key . '}}', $value, $template);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $template;
|
return $template;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for easy access
|
function emailService(): Email {
|
||||||
function sendEmail(): SendGridEmail {
|
|
||||||
static $instance = null;
|
static $instance = null;
|
||||||
if ($instance === null) {
|
if ($instance === null) {
|
||||||
$instance = new SendGridEmail();
|
$instance = new Email();
|
||||||
}
|
}
|
||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-19
@@ -329,33 +329,29 @@ function getCartTotal() {
|
|||||||
* Send email via CyberMail API
|
* Send email via CyberMail API
|
||||||
*/
|
*/
|
||||||
function sendEmail($to, $subject, $htmlContent, $textContent = '') {
|
function sendEmail($to, $subject, $htmlContent, $textContent = '') {
|
||||||
$apiKey = defined('CYBERMAIL_API_KEY') ? CYBERMAIL_API_KEY : '';
|
$apiKey = getSetting('cybermail_api_key', defined('CYBERMAIL_API_KEY') ? CYBERMAIL_API_KEY : '');
|
||||||
if (!$apiKey) return false;
|
$from = getSetting('cybermail_from_email', 'noreply@tomsjavajive.com');
|
||||||
|
$fromName = getSetting('cybermail_from_name', "Tom's Java Jive");
|
||||||
$payload = [
|
if (!$apiKey) {
|
||||||
'from' => ['email' => SENDER_EMAIL, 'name' => SENDER_NAME],
|
error_log('[TJJ sendEmail] CYBERMAIL_API_KEY not configured');
|
||||||
'to' => [['email' => $to]],
|
return false;
|
||||||
'subject' => $subject,
|
}
|
||||||
'html' => $htmlContent,
|
$payload = ['from' => ['email' => $from, 'name' => $fromName],
|
||||||
];
|
'to' => [['email' => $to]], 'subject' => $subject, 'html' => $htmlContent];
|
||||||
if ($textContent) $payload['text'] = $textContent;
|
if ($textContent) $payload['text'] = $textContent;
|
||||||
|
|
||||||
$ch = curl_init('https://platform.cyberpersons.com/email/v1/send');
|
$ch = curl_init('https://platform.cyberpersons.com/email/v1/send');
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
|
||||||
CURLOPT_POST => true,
|
|
||||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
CURLOPT_HTTPHEADER => [
|
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json'],
|
||||||
'Authorization: Bearer ' . $apiKey,
|
CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => false,
|
||||||
'Content-Type: application/json',
|
|
||||||
],
|
|
||||||
CURLOPT_TIMEOUT => 20,
|
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
|
||||||
]);
|
]);
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
return $httpCode === 202;
|
if ($httpCode === 202) return true;
|
||||||
|
error_log('[TJJ sendEmail] CyberMail HTTP ' . $httpCode . ' — ' . $response);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user