Files
mailer-dev-console/htdocs/app/preview/outbox.php

539 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../mail/mailer.php';
require_once __DIR__ . '/../mail/templates/loader.php';
$selectedTemplate = $_POST['tpl'] ?? $_GET['tpl'] ?? 'quary';
$validTemplates = array_keys(mailTemplateMap());
if (!in_array($selectedTemplate, $validTemplates, true)) {
$selectedTemplate = 'quary';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sent = sendAccountCreationMail(false, $selectedTemplate);
$code = $sent ? 'ok' : 'fail';
header('Location: ' . $_SERVER['PHP_SELF'] . '?status=' . $code . '&tpl=' . urlencode($selectedTemplate));
exit;
}
sendAccountCreationMail(true, $selectedTemplate);
$subject = $GLOBALS['MAIL_PREVIEW_SUBJECT'] ?? '(no subject)';
$body = $GLOBALS['MAIL_PREVIEW_BODY'] ?? '';
$to = $GLOBALS['MAIL_PREVIEW_TO'] ?? '';
$username = $GLOBALS['MAIL_PREVIEW_USERNAME'] ?? '';
$domain = $GLOBALS['MAIL_PREVIEW_DOMAIN'] ?? '';
$company = $GLOBALS['MAIL_PREVIEW_COMPANY'] ?? '';
$github = $GLOBALS['MAIL_PREVIEW_GITHUB'] ?? '';
$website = $GLOBALS['MAIL_PREVIEW_WEBSITE'] ?? '';
$fromEmail = $GLOBALS['MAIL_PREVIEW_FROM'] ?? '';
$fromName = $GLOBALS['MAIL_PREVIEW_FROM_NAME'] ?? '';
$smtpHost = $GLOBALS['MAIL_PREVIEW_MAIL_HOST'] ?? '';
$smtpUser = $GLOBALS['MAIL_PREVIEW_MAIL_USERNAME'] ?? '';
$smtpPort = $GLOBALS['MAIL_PREVIEW_MAIL_PORT'] ?? '';
$smtpEncryption = $GLOBALS['MAIL_PREVIEW_MAIL_ENCRYPTION'] ?? '';
$smtpPassword = $GLOBALS['MAIL_PREVIEW_MAIL_PASSWORD'] ?? '';
$projectStart = $GLOBALS['MAIL_PREVIEW_PROJECT_START'] ?? '';
$currentYear = $GLOBALS['MAIL_PREVIEW_CURRENT_YEAR'] ?? '';
$templateIndex = $GLOBALS['MAIL_PREVIEW_TEMPLATES_INDEX'] ?? [];
$templateLabel = $GLOBALS['MAIL_PREVIEW_TEMPLATE_LABEL'] ?? '';
$templateKey = $GLOBALS['MAIL_PREVIEW_TEMPLATE_KEY'] ?? $selectedTemplate;
/**
* Build a full state snapshot so ANY change (template or mail config)
* will change the hash and trigger a live refresh.
*/
$previewState = [
'body' => $body,
'subject' => $subject,
'to' => $to,
'username' => $username,
'domain' => $domain,
'company' => $company,
'fromName' => $fromName,
'fromEmail' => $fromEmail,
'smtpHost' => $smtpHost,
'smtpUser' => $smtpUser,
'smtpPort' => $smtpPort,
'smtpEncryption' => $smtpEncryption,
// we only care if a password exists, not the value itself
'hasSmtpPassword' => (bool) $smtpPassword,
'projectStart' => $projectStart,
'currentYear' => $currentYear,
];
$bodyHash = sha1(json_encode($previewState, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
if (isset($_GET['mode']) && $_GET['mode'] === 'json') {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'hash' => $bodyHash,
'state' => $previewState,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/**
* Status banner state
*/
$statusKind = null; // 'success' | 'error' | null
$statusMessage = null;
if (isset($_GET['status'])) {
if ($_GET['status'] === 'ok') {
$statusKind = 'success';
$statusMessage = "Email sent successfully to {$to}";
} elseif ($_GET['status'] === 'fail') {
$statusKind = 'error';
$statusMessage = "Failed to send email. Check error logs.";
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Mail Renderer - Xovae</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@300;400;500;600;700&display=swap"
rel="stylesheet" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['"Lexend Deca"', 'system-ui', 'sans-serif'],
}
}
}
}
</script>
<style>
.grid-pattern {
background-image:
linear-gradient(to right, rgba(148, 163, 184, 0.08) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.08) 1px, transparent 1px);
background-size: 24px 24px;
}
</style>
</head>
<body class="bg-slate-50 text-slate-900 min-h-screen font-sans antialiased">
<div class="min-h-screen flex flex-col">
<header class="bg-slate-900 text-white border-b border-slate-700">
<div class="max-w-[1800px] mx-auto px-6 py-6">
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-4">
<div
class="flex items-center justify-center w-12 h-12 bg-blue-600 text-white font-bold text-lg rounded-md">
<?= strtoupper(substr($domain ?: 'UD', 0, 2)) ?>
</div>
<div>
<h1 class="text-2xl font-semibold tracking-tight" id="domainTitle">
<?= htmlspecialchars($domain ?: 'MAIL PREVIEW CONSOLE') ?>
</h1>
<p class="text-xs font-medium text-slate-400 tracking-[0.18em] mt-0.5">
Mail Template Review & Delivery Workspace
</p>
</div>
</div>
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<form method="get"
class="flex items-center gap-3 bg-slate-800/90 border border-slate-700 px-4 py-2.5 rounded-lg">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider">
TEMPLATE
</label>
<div class="relative">
<select name="tpl" class="appearance-none bg-slate-700 border border-slate-600 text-white text-sm
pl-3 pr-8 py-1.5 rounded-md
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
cursor-pointer" onchange="this.form.submit()">
<?php foreach ($templateIndex as $tpl): ?>
<option value="<?= htmlspecialchars($tpl['key']) ?>"
<?= $tpl['key'] === $templateKey ? 'selected' : '' ?>>
<?= htmlspecialchars($tpl['label']) ?>
</option>
<?php endforeach; ?>
</select>
<svg class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7" />
</svg>
</div>
</form>
<form method="post" onsubmit="handleSendEmail(event)">
<input type="hidden" name="tpl" value="<?= htmlspecialchars($templateKey) ?>" />
<button type="submit" name="send" id="sendButton" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded font-medium text-sm
transition-colors focus:outline-none focus:ring-2
focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-900
flex items-center gap-2">
<svg id="sendIcon" class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
<svg id="loadingIcon" class="w-4 h-4 animate-spin hidden" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0
3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<span id="sendText">Send Email</span>
</button>
</form>
</div>
</div>
</div>
</header>
<?php if ($statusKind && $statusMessage): ?>
<div class="bg-white border-b border-slate-200">
<div class="max-w-[1800px] mx-auto px-6 py-3">
<div
class="flex items-center gap-3 <?= $statusKind === 'success' ? 'text-green-700' : 'text-red-700' ?>">
<div
class="flex items-center justify-center w-8 h-8 rounded-full <?= $statusKind === 'success' ? 'bg-green-100' : 'bg-red-100' ?>">
<span class="text-lg font-bold">
<?= $statusKind === 'success' ? '✓' : '✕' ?>
</span>
</div>
<p class="font-medium text-sm"><?= htmlspecialchars($statusMessage) ?></p>
</div>
</div>
</div>
<?php endif; ?>
<div class="flex-1 max-w-[1800px] mx-auto w-full px-6 py-6">
<div class="grid grid-cols-1 xl:grid-cols-[380px,minmax(0,1fr)] gap-6 h-full">
<section class="bg-white border border-slate-200 shadow-sm rounded-lg overflow-hidden flex flex-col">
<div class="bg-slate-800 text-white px-5 py-4 border-b border-slate-700">
<h2 class="font-semibold text-sm tracking-wider">
Message Configurations From .env
</h2>
</div>
<div class="flex-1 overflow-y-auto px-5 py-5 space-y-6 text-sm">
<div>
<h3
class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3 pb-2 border-b border-slate-200">
Message Details
</h3>
<dl class="space-y-3">
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
From
</dt>
<dd class="text-slate-900">
<div class="font-medium" id="fromNameValue">
<?= htmlspecialchars($fromName ?: 'N/A') ?>
</div>
<div class="text-xs text-slate-500" id="fromEmailValue">
<?= htmlspecialchars($fromEmail ?: 'N/A') ?>
</div>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
To
</dt>
<dd class="text-slate-900">
<div class="font-medium" id="toNameValue">
<?= htmlspecialchars($username ?: 'User') ?>
</div>
<div class="text-xs text-slate-500" id="toEmailValue">
<?= htmlspecialchars($to ?: 'N/A') ?>
</div>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Subject
</dt>
<dd class="font-medium text-slate-900" id="subjectValue">
<?= htmlspecialchars($subject) ?>
</dd>
</div>
</dl>
</div>
<div>
<h3
class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3 pb-2 border-b border-slate-200">
Organization
</h3>
<dl class="space-y-3">
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Domain
</dt>
<dd class="font-medium text-slate-900" id="domainValue">
<?= htmlspecialchars($domain ?: '—') ?>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Company
</dt>
<dd class="font-medium text-slate-900" id="companyValue">
<?= htmlspecialchars($company ?: '—') ?>
</dd>
</div>
</dl>
</div>
<div>
<h3
class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3 pb-2 border-slate-200 border-b">
SMTP Configuration
</h3>
<dl class="space-y-3">
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Host
</dt>
<dd class="text-xs text-slate-900" id="smtpHostValue">
<?= htmlspecialchars($smtpHost ?: '—') ?>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Username
</dt>
<dd class="text-xs text-slate-900" id="smtpUserValue">
<?= htmlspecialchars($smtpUser ?: '—') ?>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Port
</dt>
<dd class="text-xs text-slate-900" id="smtpPortValue">
<?= htmlspecialchars($smtpPort ?: '—') ?>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Encryption
</dt>
<dd class="text-xs text-slate-900" id="smtpEncValue">
<?= htmlspecialchars($smtpEncryption ?: '—') ?>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Password
</dt>
<dd class="text-xs text-slate-500" id="smtpPwdValue">
<?= htmlspecialchars($smtpPassword ? '••••••••' : '—') ?>
</dd>
</div>
<div>
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
Project Timeline
</dt>
<dd class="text-xs text-slate-900" id="projectTimelineValue">
<?php if ($projectStart && $currentYear): ?>
<?= htmlspecialchars($projectStart) ?> <?= htmlspecialchars($currentYear) ?>
<?php else: ?>
<span class="text-slate-400">—</span>
<?php endif; ?>
</dd>
</div>
</dl>
</div>
<div class="bg-slate-50 border border-slate-200 rounded p-4">
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-700 mb-2">
Documentation
</h3>
<p class="text-xs leading-relaxed text-slate-600">
Preview live-rendered HTML email templates are generated by PHPMailer.
To modify a template, edit
<code class="text-[10px] bg-white px-1.5 py-0.5 border border-slate-300 rounded">
app/mail/mailer.php
</code>
and manage template definitions in
<code class="text-[10px] bg-white px-1.5 py-0.5 border border-slate-300 rounded">
app/mail/templates/loader.php
</code>.
For more, visit
<a href="https://github.com/xodivorce/mailer-dev-console" target="_blank"
class="text-[#0f172a] hover:text-[#0f172acb] underline">
xodivorce/mailer-dev-console
</a>.
</p>
</div>
</div>
</section>
<section
class="bg-white border border-slate-200 shadow-sm rounded-lg overflow-hidden flex flex-col min-h-[700px]">
<div class="bg-slate-100 border-b border-slate-200 px-5 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div id="livePreviewBadge"
class="flex items-center justify-center w-10 h-8 bg-slate-700 text-white text-xs rounded transition-colors duration-200">
HTML
</div>
<div>
<h2 class="font-semibold text-slate-900 text-base">
Rendering Console
</h2>
<p id="previewSubjectLine" class="text-xs text-slate-500 mt-0.5 leading-snug">
<?= htmlspecialchars($subject) ?>
</p>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs font-semibold text-slate-600 tracking-wider">
iFRAME RENDERING..
</span>
<span class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
</div>
</div>
</div>
<div class="flex-1 grid-pattern bg-slate-50 p-2">
<iframe id="previewFrame"
class="w-full h-full bg-white border border-slate-200 rounded shadow-inner"
srcdoc="<?= htmlspecialchars($body, ENT_QUOTES, 'UTF-8') ?>"></iframe>
</div>
</section>
</div>
</div>
</div>
<script>
const MAILER_INITIAL_HASH = "<?= htmlspecialchars($bodyHash, ENT_QUOTES, 'UTF-8') ?>";
const MAILER_TPL_KEY = "<?= htmlspecialchars($templateKey, ENT_QUOTES, 'UTF-8') ?>";
const MAILER_PHP_SELF = "<?= htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') ?>";
let currentHash = MAILER_INITIAL_HASH;
function handleSendEmail(event) {
const button = document.getElementById('sendButton');
const sendIcon = document.getElementById('sendIcon');
const loadingIcon = document.getElementById('loadingIcon');
const sendText = document.getElementById('sendText');
button.disabled = true;
button.classList.add('opacity-75', 'cursor-not-allowed');
button.classList.remove('hover:bg-blue-700');
sendIcon.classList.add('hidden');
loadingIcon.classList.remove('hidden');
sendText.textContent = 'Processing...';
}
function flashLivePreviewBadge() {
const badge = document.getElementById('livePreviewBadge');
if (!badge) return;
badge.classList.remove('bg-slate-700');
badge.classList.add('bg-green-600');
setTimeout(() => {
badge.classList.remove('bg-green-600');
badge.classList.add('bg-slate-700');
}, 600);
}
function setText(id, value) {
const el = document.getElementById(id);
if (el) {
el.textContent = value;
}
}
async function pollTemplateChanges() {
try {
const params = new URLSearchParams({
tpl: MAILER_TPL_KEY,
mode: 'json',
_: Date.now().toString(),
});
const res = await fetch(MAILER_PHP_SELF + '?' + params.toString(), {
cache: 'no-store',
});
if (!res.ok) throw new Error('Preview poll failed');
const data = await res.json();
if (!data || !data.hash || !data.state) return;
if (data.hash !== currentHash) {
currentHash = data.hash;
const s = data.state;
// iframe + subject
const iframe = document.getElementById('previewFrame');
const subjectLine = document.getElementById('previewSubjectLine');
if (iframe && typeof s.body === 'string') {
iframe.srcdoc = s.body;
}
if (subjectLine && typeof s.subject === 'string') {
subjectLine.textContent = s.subject || '(no subject)';
}
setText('subjectValue', s.subject || '(no subject)');
// message details
setText('fromNameValue', s.fromName || 'N/A');
setText('fromEmailValue', s.fromEmail || 'N/A');
setText('toNameValue', s.username || 'User');
setText('toEmailValue', s.to || 'N/A');
// org
setText('domainValue', s.domain || '—');
setText('companyValue', s.company || '—');
// header title domain
const domainTitle = document.getElementById('domainTitle');
if (domainTitle) {
domainTitle.textContent = s.domain || 'MAIL PREVIEW CONSOLE';
}
// smtp
setText('smtpHostValue', s.smtpHost || '—');
setText('smtpUserValue', s.smtpUser || '—');
setText('smtpPortValue', s.smtpPort || '—');
setText('smtpEncValue', s.smtpEncryption || '—');
setText('smtpPwdValue', s.hasSmtpPassword ? '••••••••' : '—');
// project timeline
const timeline = (s.projectStart && s.currentYear)
? (s.projectStart + ' ' + s.currentYear)
: '—';
setText('projectTimelineValue', timeline);
flashLivePreviewBadge();
}
} catch (err) {
// silent fail, retry
} finally {
setTimeout(pollTemplateChanges, 2000);
}
}
window.addEventListener('DOMContentLoaded', () => {
pollTemplateChanges();
});
</script>
</body>
</html>