Signup Email Tester (Catch & View Emails)
A lightweight signup email tester you can run on your own site to test registration flows. It gives you disposable inbox addresses (like test+abc@yourdomain.com), captures outgoing signup emails (verification links, reset emails, OTP codes), and lets you view them in a simple inbox UI.
The catch: This tool works best when your app sends mail via PHP’s mail() (or you can change your mail-sending function to write to this tester during dev). If your app uses external SMTP APIs, you can still use this by switching to “dev mode” and routing messages into the catcher endpoint.
What it does:
- Generates “disposable” test inboxes (unique addresses per test).
- Catches emails via a simple HTTP endpoint (dev mode) OR via your own mail wrapper.
- Shows a clean inbox with subject, to/from, timestamp, and body.
- Extracts links and common OTP codes so you can click fast.
- Stores messages as JSON files (no database).
Install:
- Create:
/tools/mailtest/ - Create folders (writable):
/tools/mailtest/_db/
- Save the script below as:
/tools/mailtest/index.php - Set your
ADMIN_TOKENnear the top. - Use it:
- Inbox UI:
/tools/mailtest/?admin=TOKEN - Catch endpoint (dev):
/tools/mailtest/?catch=TOKEN
- Inbox UI:
How to actually test signups:
- In your signup app, set “send email” to call this tool in dev mode (example included).
- Register with a generated inbox address (shown inside the tool).
- Open the inbox UI and click the verification link / copy the OTP.
<?php
declare(strict_types=1);
/**
* Signup Email Tester (Catch & View Emails) — No DB
* File: /tools/mailtest/index.php
*
* UI:
* /tools/mailtest/?admin=TOKEN
*
* Catch endpoint (POST JSON):
* POST /tools/mailtest/?catch=TOKEN
* { "to":"test+abc@yourdomain.com", "from":"noreply@yourdomain.com", "subject":"Verify", "body":"..." }
*
* Tip:
* - Use this only for dev/testing. Keep it protected (token + optional IP allowlist).
*/
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
const ADMIN_TOKEN = 'CHANGE_ME_TOKEN';
const STORE_DIR = __DIR__ . '/_db';
const MAX_MSGS = 500; // retention cap
// Optional: lock down by IP (leave empty to allow any)
const ALLOW_IPS = [
// '123.123.123.123',
];
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
function ensure_dir(string $dir): bool { return is_dir($dir) || @mkdir($dir, 0755, true); }
function ok_ip(): bool {
if (!ALLOW_IPS) return true;
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
return in_array($ip, ALLOW_IPS, true);
}
function base_url(): string {
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && stripos((string)$_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0);
$proto = $https ? 'https://' : 'http://';
$host = $_SERVER['HTTP_HOST'] ?? '';
$path = $_SERVER['SCRIPT_NAME'] ?? '';
return $proto . $host . $path;
}
function json_read(string $file): array {
if (!is_file($file)) return [];
$raw = @file_get_contents($file);
if ($raw === false) return [];
$j = json_decode($raw, true);
return is_array($j) ? $j : [];
}
function json_write(string $file, array $data): bool {
$out = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($out === false) return false;
return @file_put_contents($file, $out, LOCK_EX) !== false;
}
function list_msgs(): array {
$files = glob(STORE_DIR . '/*.json') ?: [];
rsort($files);
$out = [];
foreach ($files as $f) {
$j = json_read($f);
if (!$j) continue;
$j['_file'] = basename($f);
$out[] = $j;
}
return $out;
}
function prune_old(): void {
$files = glob(STORE_DIR . '/*.json') ?: [];
if (count($files) <= MAX_MSGS) return;
sort($files); // oldest first
$extra = count($files) - MAX_MSGS;
for ($i=0; $i<$extra; $i++) @unlink($files[$i]);
}
function extract_links(string $body): array {
preg_match_all('~https?://[^\s<>"\']+~i', $body, $m);
$links = $m[0] ?? [];
$links = array_values(array_unique($links));
return array_slice($links, 0, 20);
}
function extract_otp(string $body): string {
// common patterns: "123456" or "Code: 123456"
if (preg_match('/\b(\d{4,8})\b/', $body, $m)) return (string)$m[1];
return '';
}
ensure_dir(STORE_DIR);
if (!ok_ip()) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo "Forbidden (IP).\n";
exit;
}
// ---------------- CATCH ENDPOINT ----------------
if (isset($_GET['catch'])) {
$tok = (string)($_GET['catch'] ?? '');
if ($tok === '' || !hash_equals(ADMIN_TOKEN, $tok)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo "Forbidden.\n";
exit;
}
$raw = (string)file_get_contents('php://input');
$j = json_decode($raw, true);
if (!is_array($j)) {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok'=>false,'error'=>'Expected JSON.'], JSON_UNESCAPED_SLASHES);
exit;
}
$to = trim((string)($j['to'] ?? ''));
$from = trim((string)($j['from'] ?? ''));
$subject = trim((string)($j['subject'] ?? ''));
$body = (string)($j['body'] ?? '');
if ($to === '' || $subject === '' || $body === '') {
http_response_code(422);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok'=>false,'error'=>'Missing to/subject/body.'], JSON_UNESCAPED_SLASHES);
exit;
}
$id = gmdate('Ymd_His') . '_' . bin2hex(random_bytes(6));
$msg = [
'id' => $id,
'to' => $to,
'from' => $from ?: 'noreply@localhost',
'subject' => $subject,
'body' => $body,
'links' => extract_links($body),
'otp' => extract_otp($body),
'createdAt' => gmdate('c'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
];
json_write(STORE_DIR . '/' . $id . '.json', $msg);
prune_old();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok'=>true,'id'=>$id], JSON_UNESCAPED_SLASHES);
exit;
}
// ---------------- ADMIN UI ----------------
$tok = (string)($_GET['admin'] ?? '');
if ($tok === '' || !hash_equals(ADMIN_TOKEN, $tok)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo "Forbidden.\n";
exit;
}
$all = list_msgs();
$view = (string)($_GET['view'] ?? '');
$view = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $view) ?? '';
$current = $view ? json_read(STORE_DIR . '/' . $view) : [];
$domain = $_SERVER['HTTP_HOST'] ?? 'yourdomain.com';
$sampleAddr = 'test+' . substr(sha1((string)time()), 0, 6) . '@' . $domain;
?><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Signup Email Tester</title>
<meta name="robots" content="noindex,nofollow" />
<style>
:root{
--bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
--text:#e8eef8; --muted:rgba(232,238,248,.72);
--line:rgba(255,255,255,.10); --accent:#2a8cff;
--shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.wrap{max-width:1100px;margin:0 auto;padding:18px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
.muted{color:var(--muted)}
.pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
.grid{display:grid;grid-template-columns:360px 1fr;gap:12px}
@media(max-width:980px){.grid{grid-template-columns:1fr}}
a{color:#8ad1ff;text-decoration:none}
a:hover{text-decoration:underline}
.item{padding:10px;border-radius:14px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.03);margin:10px 0}
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
pre{margin:0;padding:12px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);overflow:auto;white-space:pre-wrap}
.btn2{display:inline-flex;gap:8px;align-items:center;border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:transparent;color:var(--text);font-weight:900;cursor:pointer}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="pill">Dev Tool</div>
<h1 style="margin:10px 0 6px;font-size:22px">Signup Email Tester</h1>
<div class="muted">Catch signup emails locally and view verification links + OTP codes. Keep this private.</div>
<div class="muted" style="margin-top:8px">
Catch endpoint: <span class="code"><?php echo h(base_url() . '?catch=TOKEN'); ?></span>
</div>
<div class="muted" style="margin-top:8px">
Quick disposable address idea: <span class="code"><?php echo h($sampleAddr); ?></span>
</div>
</div>
<div class="grid">
<div class="card">
<b>Inbox</b>
<div class="muted" style="margin-top:6px">Newest first. Click to view.</div>
<?php if (!$all): ?>
<div class="muted" style="margin-top:10px">No messages yet.</div>
<?php endif; ?>
<?php foreach (array_slice($all, 0, 80) as $m): ?>
<div class="item">
<div style="font-weight:900">
<a href="?admin=<?php echo h($tok); ?>&view=<?php echo h((string)$m['_file']); ?>">
<?php echo h((string)($m['subject'] ?? '(no subject)')); ?>
</a>
</div>
<div class="muted" style="font-size:12px;margin-top:4px">
To: <span class="code"><?php echo h((string)($m['to'] ?? '')); ?></span><br>
<span class="code"><?php echo h((string)($m['createdAt'] ?? '')); ?></span>
<?php if (!empty($m['otp'])): ?> • OTP: <b><?php echo h((string)$m['otp']); ?></b><?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="card">
<b>Message</b>
<?php if (!$current): ?>
<div class="muted" style="margin-top:10px">Select a message from the inbox.</div>
<?php else: ?>
<div class="muted" style="margin-top:8px">
<div><b>To:</b> <span class="code"><?php echo h((string)($current['to'] ?? '')); ?></span></div>
<div><b>From:</b> <span class="code"><?php echo h((string)($current['from'] ?? '')); ?></span></div>
<div><b>Date:</b> <span class="code"><?php echo h((string)($current['createdAt'] ?? '')); ?></span></div>
<?php if (!empty($current['otp'])): ?>
<div style="margin-top:6px"><b>OTP:</b> <span class="code"><?php echo h((string)$current['otp']); ?></span></div>
<?php endif; ?>
</div>
<?php if (!empty($current['links']) && is_array($current['links'])): ?>
<div style="margin-top:12px">
<b>Links found</b>
<div class="muted" style="margin-top:6px">
<?php foreach ($current['links'] as $lnk): ?>
<div class="code"><a target="_blank" rel="noopener" href="<?php echo h((string)$lnk); ?>"><?php echo h((string)$lnk); ?></a></div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div style="margin-top:12px">
<b>Body</b>
<pre class="code"><?php echo h((string)($current['body'] ?? '')); ?></pre>
</div>
<?php endif; ?>
<div style="margin-top:12px">
<b>Dev mode send example</b>
<div class="muted" style="margin-top:6px;font-size:13px;line-height:1.45">
Instead of sending real email, POST your message JSON to the catcher:
</div>
<pre class="code"><?php echo h('<?php
$catch = "https://your-site.com/tools/mailtest/?catch=CHANGE_ME_TOKEN";
$payload = [
"to" => $email,
"from" => "noreply@yourdomain.com",
"subject" => "Verify your account",
"body" => "Click: https://your-site.com/verify?token=XYZ\nOTP: 123456",
];
$ch = curl_init($catch);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 6,
]);
$res = curl_exec($ch);
curl_close($ch);
?>'); ?></pre>
</div>
</div>
</div>
</div>
</body>
</html>
Comments (0)
No comments yet — be the first.