Redesign landing page, JSON caller response, shared email shell

- docs.htm: dark single-page redesign in plain CSS, live URL builder,
  all four parsers documented, Bootstrap CDN dependency dropped
- index.php: respond with pretty-printed JSON (success and 400 error);
  email "Parsed Variables" block uses JSON instead of print_r
- parsers/helpers.php: emailShell() provides one stylesheet, header and
  footer for every email template; five duplicated stylesheets removed
This commit is contained in:
Andy Dixon
2026-09-11 18:23:25 +01:00
parent f3f58dbf95
commit c1ec1adf85
7 changed files with 602 additions and 1503 deletions
+267 -561
View File
@@ -1,587 +1,293 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webhook Email Forwarder - Documentation</title> <title>Webhook → Email | <?php echo $_SERVER['HTTP_HOST']; ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <meta name="description" content="Point any webhook at a URL with your email in it. Get the whole request in your inbox. That's it. That's the product.">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> <style>
<style> :root{
body { --bg:#0b0b10;--bg2:#12121a;--panel:#161622;--line:#26263a;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --text:#ececf4;--muted:#9a9ab4;--dim:#6a6a86;
min-height: 100vh; --a1:#8b5cf6;--a2:#ec4899;--a3:#f97316;
padding: 40px 0; --grad:linear-gradient(110deg,var(--a1),var(--a2) 55%,var(--a3));
} --mono:ui-monospace,SFMono-Regular,"JetBrains Mono",Menlo,Consolas,monospace;
.doc-container { --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,"Helvetica Neue",Arial,sans-serif;
background: white; }
border-radius: 15px; *{box-sizing:border-box}
box-shadow: 0 10px 40px rgba(0,0,0,0.2); html{scroll-behavior:smooth}
padding: 40px; body{
margin-bottom: 40px; margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);
} line-height:1.6;font-size:17px;-webkit-font-smoothing:antialiased;
.hero { background-image:
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); radial-gradient(900px 500px at 15% -10%,rgba(139,92,246,.22),transparent 60%),
color: white; radial-gradient(700px 400px at 90% 10%,rgba(236,72,153,.16),transparent 60%),
padding: 60px 40px; linear-gradient(var(--line) 1px,transparent 1px),
border-radius: 15px; linear-gradient(90deg,var(--line) 1px,transparent 1px);
margin-bottom: 40px; background-size:auto,auto,48px 48px,48px 48px;
text-align: center; background-attachment:fixed;
} }
.code-block { a{color:inherit}
background: #f8f9fa; .wrap{max-width:980px;margin:0 auto;padding:0 20px}
border-left: 4px solid #667eea; ::selection{background:var(--a2);color:#fff}
padding: 15px;
border-radius: 5px; /* nav */
font-family: 'Courier New', monospace; nav{display:flex;align-items:center;justify-content:space-between;padding:22px 0;font-size:14px}
overflow-x: auto; nav .brand{font-family:var(--mono);font-weight:700;letter-spacing:-.02em}
} nav .brand span{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
.feature-card { nav ul{display:flex;gap:22px;list-style:none;margin:0;padding:0;color:var(--muted)}
border: none; nav a{text-decoration:none}nav a:hover{color:var(--text)}
box-shadow: 0 4px 6px rgba(0,0,0,0.1); @media(max-width:600px){nav ul{display:none}}
transition: transform 0.2s;
height: 100%; /* hero */
} .hero{padding:70px 0 40px;position:relative}
.feature-card:hover { .eyebrow{font-family:var(--mono);font-size:13px;color:var(--a2);letter-spacing:.12em;text-transform:uppercase;margin-bottom:18px}
transform: translateY(-5px); h1{font-size:clamp(2.6rem,7vw,5.2rem);line-height:.98;letter-spacing:-.04em;margin:0 0 22px;font-weight:800}
} h1 em{font-style:normal;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
.badge-custom { .hero p{font-size:clamp(1.05rem,2vw,1.35rem);color:var(--muted);max-width:640px;margin:0 0 30px}
font-size: 0.85rem; .hero p b{color:var(--text);font-weight:600}
padding: 0.5em 0.8em;
} /* url builder */
h2 { .builder{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:18px;box-shadow:0 30px 80px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.04)}
color: #667eea; .builder label{display:block;font-size:12px;color:var(--dim);font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px}
margin-top: 30px; .builder input{
margin-bottom: 20px; width:100%;font:inherit;font-size:1.1rem;padding:12px 14px;border-radius:10px;
} background:var(--bg);border:1px solid var(--line);color:var(--text);outline:none;
h3 { }
color: #764ba2; .builder input:focus{border-color:var(--a1);box-shadow:0 0 0 3px rgba(139,92,246,.25)}
margin-top: 25px; .url{margin-top:14px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
margin-bottom: 15px; .url code{flex:1;min-width:0;font-family:var(--mono);font-size:.95rem;word-break:break-all;padding:12px 14px;border-radius:10px;background:var(--bg);border:1px dashed var(--line)}
} .url code b{color:var(--a3);font-weight:600}
.alert-info { button.copy{font:inherit;font-size:.9rem;font-weight:600;padding:12px 18px;border-radius:10px;border:0;cursor:pointer;background:var(--grad);color:#fff;white-space:nowrap}
border-left: 4px solid #0dcaf0; button.copy:active{transform:scale(.97)}
} .hint{font-size:.85rem;color:var(--dim);margin:12px 0 0}
.alert-warning {
border-left: 4px solid #ffc107; /* sections */
} section{padding:56px 0;border-top:1px solid var(--line)}
.alert-success { h2{font-size:clamp(1.7rem,4vw,2.4rem);letter-spacing:-.03em;margin:0 0 8px;line-height:1.1}
border-left: 4px solid #198754; .sub{color:var(--muted);margin:0 0 28px;max-width:640px}
} h3{font-size:1.1rem;margin:28px 0 10px;letter-spacing:-.01em}
</style> .tangent{color:var(--dim);font-size:.92rem;font-style:italic}
p code,li code{font-family:var(--mono);font-size:.88em;background:var(--panel);border:1px solid var(--line);padding:2px 6px;border-radius:6px;color:#fbbf24}
pre{
margin:0;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;
font-family:var(--mono);font-size:.9rem;line-height:1.55;overflow-x:auto;color:#d6d6e6;
position:relative
}
pre::before{content:attr(data-lang);position:absolute;top:8px;right:12px;font-size:11px;color:var(--dim);letter-spacing:.1em;text-transform:uppercase}
pre .h{color:#f9a8d4}pre .s{color:#fcd34d}pre .k{color:#a5b4fc}
.stack{display:grid;gap:14px}
/* steps */
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;counter-reset:s}
.step{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px;position:relative;counter-increment:s}
.step::before{content:"0" counter(s);font-family:var(--mono);font-size:2.4rem;font-weight:800;line-height:1;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent;display:block;margin-bottom:12px}
.step b{display:block;font-size:1.05rem;margin-bottom:4px}
.step span{color:var(--muted);font-size:.95rem}
/* parsers */
.parsers{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}
.parser{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;transition:transform .15s,border-color .15s}
.parser:hover{transform:translateY(-3px);border-color:var(--a1)}
.parser .ico{font-size:1.6rem;margin-bottom:8px}
.parser code{display:inline-block;font-family:var(--mono);font-size:.85rem;color:var(--a3);margin-bottom:8px}
.parser p{margin:0;color:var(--muted);font-size:.95rem}
/* inbox list */
.inbox{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;list-style:none;padding:0;margin:0}
.inbox li{padding:14px 16px;background:var(--panel);border:1px solid var(--line);border-radius:12px;font-size:.95rem}
.inbox li b{display:block;color:var(--text)}
.inbox li span{color:var(--muted)}
/* callouts */
.note{border-left:3px solid var(--a1);padding:14px 18px;background:rgba(139,92,246,.08);border-radius:0 12px 12px 0;margin:22px 0}
.note.warn{border-color:var(--a3);background:rgba(249,115,22,.08)}
.note b{color:var(--text)}
.note p{margin:0;color:var(--muted)}
/* chips */
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.chip{font-family:var(--mono);font-size:.8rem;padding:5px 11px;border:1px solid var(--line);border-radius:999px;color:var(--muted)}
footer{padding:50px 0 60px;border-top:1px solid var(--line);color:var(--dim);font-size:.9rem;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
footer a{color:var(--muted)}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="wrap">
<!-- Hero Section -->
<div class="hero">
<h1 class="display-4"><i class="bi bi-webhook"></i> Webhook Email Forwarder</h1>
<p class="lead">A simple, elegant solution for debugging webhooks by forwarding them directly to your email</p>
<p class="mb-0">Version 1.0 | Made with ❤️ by Andy Dixon</p>
</div>
<!-- Overview Section --> <nav>
<div class="doc-container"> <div class="brand"><span>webhook</span>→email</div>
<h2><i class="bi bi-info-circle"></i> Overview</h2> <ul>
<p class="lead">This webhook service captures incoming HTTP requests and forwards complete request details (headers, body, metadata) to your specified email address.</p> <li><a href="#start">Start</a></li>
<li><a href="#examples">Examples</a></li>
<div class="row mt-4"> <li><a href="#parsers">Parsers</a></li>
<div class="col-md-4 mb-3"> <li><a href="#inbox">Inbox</a></li>
<div class="card feature-card"> <li><a href="#faq">Caveats</a></li>
<div class="card-body text-center"> </ul>
<i class="bi bi-envelope-check text-primary" style="font-size: 3rem;"></i> </nav>
<h5 class="card-title mt-3">Email Delivery</h5>
<p class="card-text">Receive webhook data directly in your inbox</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body text-center">
<i class="bi bi-shield-check text-success" style="font-size: 3rem;"></i>
<h5 class="card-title mt-3">Secure</h5>
<p class="card-text">Email validation and XSS protection built-in</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body text-center">
<i class="bi bi-lightning-charge text-warning" style="font-size: 3rem;"></i>
<h5 class="card-title mt-3">Instant</h5>
<p class="card-text">Real-time webhook processing and forwarding</p>
</div>
</div>
</div>
</div>
</div>
<!-- How It Works Section --> <div class="hero">
<div class="doc-container"> <div class="eyebrow">// no signup. no dashboard. no, seriously.</div>
<h2><i class="bi bi-gear"></i> How It Works</h2> <h1>Webhooks.<br>Straight to <em>your inbox.</em></h1>
<ol class="lead"> <p>Put your email in the URL. Point a webhook at it. Every header, every byte of body, every superglobal shows up as an email. <b>Done. Go do the next thing.</b></p>
<li>You configure a third-party service to send webhooks to this URL with your email encoded in the path</li>
<li>When a webhook request arrives, the service captures all request data</li>
<li>The complete request details are formatted into a readable HTML email</li>
<li>The email is sent to your specified address</li>
<li>A confirmation response is returned to the webhook sender</li>
</ol>
<div class="alert alert-info mt-4"> <div class="builder">
<i class="bi bi-lightbulb"></i> <strong>Perfect for:</strong> Debugging webhooks, testing integrations, monitoring API callbacks, and understanding third-party service payloads. <label for="em">Type your email → get your URL</label>
</div> <input id="em" type="email" placeholder="you@example.com" autocomplete="email" spellcheck="false">
</div> <div class="url">
<code>https://<?php echo $_SERVER['HTTP_HOST']; ?>/<b id="out">you%40example.com</b></code>
<button class="copy" id="cp">Copy</button>
</div>
<p class="hint">Yes the <code>@</code> becomes <code>%40</code>. Yes that's the whole trick. Add <code>/github</code>, <code>/json</code>, etc. on the end for the fancy parsers — more on that below, if you scroll that far.</p>
</div>
</div>
<!-- Usage Section --> <section id="start">
<div class="doc-container"> <h2>Three steps. Two, really.</h2>
<h2><i class="bi bi-code-square"></i> Usage</h2> <p class="sub">The third one is just "wait for an email", which you were going to do anyway.</p>
<div class="steps">
<h3>Basic URL Format</h3> <div class="step"><b>Encode your email</b><span><code>@</code> → <code>%40</code>. The box above already did it for you. Ok moving on.</span></div>
<p>The URL format is simple - just append your URL-encoded email address to the domain:</p> <div class="step"><b>Paste the URL into whatever</b><span>Stripe, GitHub, Grafana, that cron job, a curl you're about to forget you wrote.</span></div>
<div class="code-block mb-4"> <div class="step"><b>Check your inbox</b><span>Full request, nicely formatted. Subject starts with ‼️ so it's filterable. You're welcome.</span></div>
https://<?php echo $_SERVER['HTTP_HOST']; ?>/<strong>{email-address}</strong> </div>
</div>
<h3>URL Encoding Your Email</h3> <h3>The fastest possible test</h3>
<p>The <code>@</code> symbol must be URL-encoded as <code>%40</code>:</p> <pre data-lang="bash">curl -X POST https://<?php echo $_SERVER['HTTP_HOST']; ?>/<span class="s" id="ex1">you%40example.com</span> \
-d '{"test": "hello, is this thing on"}'</pre>
<div class="row mt-3"> <p class="tangent">(the email will arrive before you've alt-tabbed back. probably. depends on your mail server. anyway.)</p>
<div class="col-md-6"> </section>
<div class="card">
<div class="card-header bg-light">
<strong>Original Email</strong>
</div>
<div class="card-body">
<code>john@example.com</code>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<strong>Webhook URL</strong>
</div>
<div class="card-body">
<code><?php echo $_SERVER['HTTP_HOST']; ?>/john%40example.com</code>
</div>
</div>
</div>
</div>
<h3 class="mt-4">Example Requests</h3> <section id="examples">
<h2>Examples, because reading is hard</h2>
<h4>cURL - POST Request with JSON</h4> <p class="sub">Any method, any content type. GET, POST, PUT, PATCH, DELETE, something you made up. JSON, form data, multipart, XML, plain text, a string of emoji. It all gets forwarded.</p>
<div class="code-block"> <div class="stack">
curl -X POST https://<?php echo $_SERVER['HTTP_HOST']; ?>/test%40example.com \ <pre data-lang="bash"><span class="h"># JSON</span>
curl -X POST https://<?php echo $_SERVER['HTTP_HOST']; ?>/<span class="s ex">you%40example.com</span> \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"event": "user.created", "user_id": 12345}' -d '{"event": "user.created", "user_id": 12345}'
</div>
<h4 class="mt-3">cURL - GET Request with Parameters</h4> <span class="h"># GET with query string (yes, this works too)</span>
<div class="code-block"> curl "https://<?php echo $_SERVER['HTTP_HOST']; ?>/<span class="s ex">you%40example.com</span>?event=test&id=123"</pre>
curl https://<?php echo $_SERVER['HTTP_HOST']; ?>/test%40example.com?event=test&id=123
</div>
<h4 class="mt-3">JavaScript - Fetch API</h4> <pre data-lang="javascript"><span class="k">fetch</span>(<span class="s">'https://<?php echo $_SERVER['HTTP_HOST']; ?>/'</span> + <span class="k">encodeURIComponent</span>(<span class="s">'you@example.com'</span>), {
<div class="code-block"> method: <span class="s">'POST'</span>,
fetch('https://<?php echo $_SERVER['HTTP_HOST']; ?>/test%40example.com', { headers: { <span class="s">'Content-Type'</span>: <span class="s">'application/json'</span> },
method: 'POST', body: JSON.stringify({ event: <span class="s">'order.completed'</span>, order_id: <span class="s">'ORD-789'</span>, total: 99.99 })
headers: { });</pre>
'Content-Type': 'application/json',
},
body: JSON.stringify({
event: 'order.completed',
order_id: 'ORD-789',
total: 99.99
})
});
</div>
<h4 class="mt-3">Python - Requests Library</h4> <pre data-lang="python"><span class="k">import</span> requests
<div class="code-block"> <span class="k">from</span> urllib.parse <span class="k">import</span> quote
import requests
response = requests.post( requests.post(
'https://<?php echo $_SERVER['HTTP_HOST']; ?>/test%40example.com', <span class="s">f'https://<?php echo $_SERVER['HTTP_HOST']; ?>/{quote("you@example.com")}'</span>,
json={'event': 'payment.received', 'amount': 250.00}, json={<span class="s">'event'</span>: <span class="s">'payment.received'</span>, <span class="s">'amount'</span>: 250.00},
headers={'X-Custom-Header': 'MyValue'} headers={<span class="s">'X-Custom-Header'</span>: <span class="s">'MyValue'</span>},
) )</pre>
print(response.text) </div>
</div> <div class="note">
<p><b>Pro tip you'll ignore and then come back for:</b> use plus addressing. <code>you+stripe%40example.com</code>, <code>you+github%40example.com</code>. Now your inbox filters do the sorting so you don't have to.</p>
</div>
</section>
<div class="alert alert-warning mt-4"> <section id="parsers">
<i class="bi bi-exclamation-triangle"></i> <strong>Important:</strong> Make sure to URL-encode your email address. Most programming languages and tools have built-in functions for this (e.g., <code>encodeURIComponent()</code> in JavaScript, <code>urllib.parse.quote()</code> in Python). <h2>Parsers. Ooh, shiny.</h2>
</div> <p class="sub">Add one word to the end of the URL and the email stops being a wall of raw JSON and starts being something you can actually read at 3am. Parser doesn't understand the payload? It falls back to the raw format. Nothing gets lost.</p>
</div> <div class="parsers">
<div class="parser"><div class="ico">🐙</div><code>/github</code><p>Push, pull request, issue and release events get their own layout with commits, authors, branches and links. Everything else still renders, just less fancy. Subjects like <em>📤 GitHub push - you/repo</em>.</p></div>
<div class="parser"><div class="ico">📈</div><code>/grafana</code><p>Grafana and Prometheus Alertmanager alerts. Firing, resolved, labels, annotations. Finally know <em>which</em> thing is on fire.</p></div>
<div class="parser"><div class="ico">🧩</div><code>/json</code><p>Any JSON at all, rendered as tables. Nested objects get split out into their own tables with path-based titles so you can find <code>data.items[3].sku</code> without squinting.</p></div>
<div class="parser"><div class="ico">💬</div><code>/wxinteract</code><p>Webex Interact SMS events. Inbound messages, delivery receipts, the lot, with the event type in the subject line.</p></div>
</div>
<!-- Response Section --> <h3>Setting up GitHub, since that's the one everyone asks about</h3>
<div class="doc-container"> <ol>
<h2><i class="bi bi-arrow-return-left"></i> What to Expect</h2> <li>Repo → Settings → Webhooks → Add webhook</li>
<li>Payload URL: <code>https://<?php echo $_SERVER['HTTP_HOST']; ?>/<span class="ex">you%40example.com</span>/github</code></li>
<h3>Immediate HTTP Response</h3> <li>Content type: <code>application/json</code></li>
<p>When your webhook is successfully received, you'll get an immediate response:</p> <li>Pick events (or "Send me everything", live a little)</li>
<div class="card"> <li>Save. Push something. Refresh inbox. Refresh again. There it is.</li>
<div class="card-header bg-success text-white"> </ol>
<strong>HTTP 200 OK</strong> </section>
</div>
<div class="card-body">
<div class="code-block">
Webhook received and forwarded to: test@example.com
Request data: <section id="inbox">
Array <h2>What actually lands in your inbox</h2>
( <p class="sub">One HTML email per request. Gmail-safe styling. All of the following, every time, whether you asked for it or not:</p>
[event] => test <ul class="inbox">
[id] => 123 <li><b>Timestamp</b><span>when it arrived, to the second</span></li>
) <li><b>Source IP + method</b><span>who sent it and how</span></li>
</div> <li><b>Content-Type</b><span>what they claimed it was</span></li>
</div> <li><b>Every header</b><span>auth, signatures, user agents, the weird custom ones</span></li>
</div> <li><b>Raw body</b><span>byte-for-byte, ready to paste into a test</span></li>
<li><b>Parsed superglobals</b><span>$_GET, $_POST, $_REQUEST, $_FILES</span></li>
</ul>
<h3>And the sender gets</h3>
<pre data-lang="http 200 · application/json">{
<span class="k">"ok"</span>: true,
<span class="k">"message"</span>: <span class="s">"Webhook received and forwarded to you@example.com"</span>,
<span class="k">"forwarded_to"</span>: <span class="s">"you@example.com"</span>,
<span class="k">"parser"</span>: <span class="s">"default"</span>,
<span class="k">"subject"</span>: <span class="s">"‼️ Webhook Request Received - 2026-09-11 17:15:42"</span>,
<span class="k">"received_at"</span>: <span class="s">"2026-09-11T17:15:42+00:00"</span>,
<span class="k">"request"</span>: { <span class="k">"method"</span>: <span class="s">"POST"</span>, <span class="k">"content_type"</span>: <span class="s">"application/json"</span>, <span class="k">"ip"</span>: <span class="s">"203.0.113.7"</span>, <span class="k">"headers"</span>: 9, <span class="k">"body_bytes"</span>: 40 },
<span class="k">"data"</span>: {
<span class="k">"query"</span>: { <span class="k">"src"</span>: <span class="s">"curl"</span> },
<span class="k">"form"</span>: {},
<span class="k">"json"</span>: { <span class="k">"event"</span>: <span class="s">"user.created"</span>, <span class="k">"user_id"</span>: 12345 },
<span class="k">"files"</span>: []
}
}</pre>
<p class="tangent">JSON bodies get decoded and echoed back under <code>data.json</code>, so you can eyeball what the server actually understood. Form fields land in <code>form</code>, query string in <code>query</code>. No print_r in sight.</p>
<p class="tangent">Any 200 is enough for most services to mark the delivery as "succeeded" and stop retrying. Which is the point.</p>
</section>
<h3 class="mt-4">Email Contents</h3> <section id="faq">
<p>Within moments, you'll receive a styled HTML email containing:</p> <h2>Caveats, small print, that sort of thing</h2>
<div class="note warn">
<div class="row mt-3"> <p><b>Anyone with the URL can email you.</b> There's no auth. There's no secret. It's your email address in a URL. Use it for debugging and testing. Do not point production payment data at it. Don't make me put a bigger warning here.</p>
<div class="col-md-6 mb-3"> </div>
<div class="card"> <h3>Stuff that's true</h3>
<div class="card-body"> <ul>
<h5 class="card-title"><i class="bi bi-clock"></i> Request Timestamp</h5> <li><b>Nothing is stored.</b> Request comes in, email goes out, request is forgotten. No logs of your payloads, no database, no "insights".</li>
<p class="card-text">Exact date and time when the webhook was received</p> <li><b>Output is escaped.</b> Everything is run through <code>htmlspecialchars()</code> before it hits the email, so a malicious payload renders as text, not as a thing.</li>
</div> <li><b>Bad email → HTTP 400, as JSON.</b> <code>{"ok": false, "error": "invalid_email", ...}</code> with a hint about <code>%40</code>. Validated with PHP's <code>FILTER_VALIDATE_EMAIL</code>. Boring, correct.</li>
</div> <li><b>Failed sends get logged.</b> On the server side, for the person running it. Not for you. Sorry.</li>
</div> </ul>
<div class="col-md-6 mb-3"> <h3>Good for</h3>
<div class="card"> <div class="chips">
<div class="card-body"> <span class="chip">debugging a webhook you didn't write</span>
<h5 class="card-title"><i class="bi bi-info-square"></i> Request Information</h5> <span class="chip">seeing what Stripe actually sends</span>
<p class="card-text">IP address, HTTP method (GET/POST/etc), and Content-Type</p> <span class="chip">capturing real payloads for docs</span>
</div> <span class="chip">testing before the handler exists</span>
</div> <span class="chip">"is it even firing?"</span>
</div> <span class="chip">alerts in your inbox without a whole thing</span>
<div class="col-md-6 mb-3"> </div>
<div class="card"> </section>
<div class="card-body">
<h5 class="card-title"><i class="bi bi-list-ul"></i> Request Headers</h5>
<p class="card-text">All HTTP headers sent with the request</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-file-earmark-text"></i> Request Body</h5>
<p class="card-text">Complete raw body content (JSON, XML, form data, etc.)</p>
</div>
</div>
</div>
<div class="col-md-12 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-code-slash"></i> PHP Variables</h5>
<p class="card-text">Parsed GET, POST, REQUEST, and FILES superglobals for easy access</p>
</div>
</div>
</div>
</div>
<div class="alert alert-success mt-4"> <footer>
<i class="bi bi-check-circle"></i> <strong>Subject Line:</strong> Emails arrive with the subject <strong>"‼️ Webhook Request Received - [timestamp]"</strong> for easy filtering and identification. <div>Built with PHP and ADHD medication. Not necessarily in that order.</div>
</div> <div>© 2025–2026 Andy Dixon · <a href="https://<?php echo $_SERVER['HTTP_HOST']; ?>/"><?php echo $_SERVER['HTTP_HOST']; ?></a></div>
</div> </footer>
<!-- Error Handling Section --> </div>
<div class="doc-container">
<h2><i class="bi bi-exclamation-octagon"></i> Error Handling</h2>
<h3>Invalid Email Address</h3>
<p>If the email address in the URL is invalid or missing, you'll receive:</p>
<div class="card">
<div class="card-header bg-danger text-white">
<strong>HTTP 400 Bad Request</strong>
</div>
<div class="card-body">
<div class="code-block">
Invalid email address provided in URL path.
</div>
</div>
</div>
<div class="alert alert-info mt-4"> <script>
<i class="bi bi-shield-check"></i> <strong>Validation:</strong> The service validates email addresses using PHP's built-in <code>FILTER_VALIDATE_EMAIL</code> filter to ensure proper formatting. (function(){
</div> var em=document.getElementById('em'),out=document.getElementById('out'),cp=document.getElementById('cp');
</div> var ex=document.querySelectorAll('.ex, #ex1');
function enc(v){return encodeURIComponent(v.trim()||'you@example.com');}
<!-- Security Section --> em.addEventListener('input',function(){
<div class="doc-container"> var e=enc(em.value);out.textContent=e;
<h2><i class="bi bi-shield-lock"></i> Security Features</h2> ex.forEach(function(n){n.textContent=e;});
<ul class="lead"> });
<li><strong>Email Validation:</strong> Only valid email addresses are accepted</li> cp.addEventListener('click',function(){
<li><strong>XSS Protection:</strong> All output is sanitized using <code>htmlspecialchars()</code></li> var url=out.parentNode.textContent.replace(/\s+/g,'');
<li><strong>No Data Storage:</strong> Webhook data is forwarded immediately and not stored on the server</li> var done=function(){cp.textContent='Copied ✓';setTimeout(function(){cp.textContent='Copy';},1400);};
<li><strong>Error Logging:</strong> Failed email attempts are logged for monitoring</li> if(navigator.clipboard){navigator.clipboard.writeText(url).then(done);}
</ul> else{var r=document.createRange();r.selectNodeContents(out.parentNode);getSelection().removeAllRanges();getSelection().addRange(r);document.execCommand('copy');done();}
});
<div class="alert alert-warning"> })();
<i class="bi bi-exclamation-triangle"></i> <strong>Privacy Note:</strong> Anyone with your webhook URL can send data to your email address. Only use this service for testing and debugging purposes. Do not use for sensitive production data without additional authentication. </script>
</div>
</div>
<!-- Supported Features Section -->
<div class="doc-container">
<h2><i class="bi bi-check2-square"></i> Supported Features</h2>
<div class="row">
<div class="col-md-6">
<h5><i class="bi bi-check-circle text-success"></i> HTTP Methods</h5>
<ul>
<li>GET</li>
<li>POST</li>
<li>PUT</li>
<li>PATCH</li>
<li>DELETE</li>
<li>Any custom method</li>
</ul>
</div>
<div class="col-md-6">
<h5><i class="bi bi-check-circle text-success"></i> Content Types</h5>
<ul>
<li>application/json</li>
<li>application/x-www-form-urlencoded</li>
<li>multipart/form-data</li>
<li>text/plain</li>
<li>text/xml</li>
<li>Any custom content type</li>
</ul>
</div>
</div>
</div>
<!-- GitHub Webhook Parser Section -->
<div class="doc-container">
<h2><i class="bi bi-github"></i> GitHub Webhook Parser</h2>
<p class="lead">Get beautifully formatted emails specifically designed for GitHub webhooks with enhanced event details.</p>
<h3>How to Use</h3>
<p>Simply add <code>/github</code> to the end of your webhook URL:</p>
<div class="code-block mb-4">
https://<?php echo $_SERVER['HTTP_HOST']; ?>/<strong>{email-address}</strong>/github
</div>
<h3>URL Format Examples</h3>
<div class="row mt-3 mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<strong>Standard Webhook</strong>
</div>
<div class="card-body">
<code><?php echo $_SERVER['HTTP_HOST']; ?>/john%40example.com</code>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-light">
<strong>GitHub Webhook</strong>
</div>
<div class="card-body">
<code><?php echo $_SERVER['HTTP_HOST']; ?>/john%40example.com/github</code>
</div>
</div>
</div>
</div>
<h3>Supported GitHub Events</h3>
<p>The GitHub parser provides enhanced formatting for these event types:</p>
<div class="row mt-3">
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">📤 Push Events</h5>
<p class="card-text">Displays branch, commit count, and individual commit messages with authors.</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">🔀 Pull Request Events</h5>
<p class="card-text">Shows PR number, title, state, branches, and direct link to the PR.</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">📝 Issue Events</h5>
<p class="card-text">Displays issue number, title, state, and link to the issue.</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">🚀 Release Events</h5>
<p class="card-text">Shows release tag, name, and direct link to the release page.</p>
</div>
</div>
</div>
</div>
<div class="alert alert-info mt-4">
<i class="bi bi-lightbulb"></i> <strong>All Events Supported:</strong> Even if an event isn't specifically formatted above, the parser will display it in a clean, structured format showing all webhook data.
</div>
<h3 class="mt-4">Setting Up in GitHub</h3>
<p>To configure a GitHub webhook:</p>
<ol>
<li>Go to your repository settings → Webhooks → Add webhook</li>
<li>Set the Payload URL to: <code>https://<?php echo $_SERVER['HTTP_HOST']; ?>/your%40email.com/github</code></li>
<li>Set Content type to: <strong>application/json</strong></li>
<li>Choose which events to receive (or select "Send me everything")</li>
<li>Click "Add webhook"</li>
</ol>
<div class="alert alert-success">
<i class="bi bi-check-circle"></i> <strong>Subject Line:</strong> GitHub webhook emails arrive with event-specific subject lines like <strong>"📤 GitHub push - username/repo"</strong> or <strong>"🔀 GitHub pull_request (opened) - username/repo"</strong> for easy filtering.
</div>
<div class="alert alert-warning">
<i class="bi bi-info-circle"></i> <strong>Tip:</strong> Use email plus addressing like <code>you+github%40example.com</code> to easily filter GitHub webhooks in your inbox.
</div>
</div>
<!-- Use Cases Section -->
<div class="doc-container">
<h2><i class="bi bi-star"></i> Common Use Cases</h2>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-bug"></i> Debugging</h5>
<p class="card-text">See exactly what data third-party services are sending to your webhooks</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-plugin"></i> Integration Testing</h5>
<p class="card-text">Test webhook integrations before implementing full handlers</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-eye"></i> Monitoring</h5>
<p class="card-text">Monitor webhook activity and payload changes over time</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-book"></i> Documentation</h5>
<p class="card-text">Capture real examples for API documentation</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-diagram-3"></i> Development</h5>
<p class="card-text">Quickly test webhook flows during development</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card feature-card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-search"></i> Troubleshooting</h5>
<p class="card-text">Diagnose issues with webhook payloads and headers</p>
</div>
</div>
</div>
</div>
</div>
<!-- Tips Section -->
<div class="doc-container">
<h2><i class="bi bi-lightbulb"></i> Tips & Best Practices</h2>
<div class="alert alert-info">
<h5><i class="bi bi-envelope-at"></i> Email Filtering</h5>
<p class="mb-0">Create an email filter for the subject line "‼️ Webhook Request Received" to organize incoming webhooks in your inbox.</p>
</div>
<div class="alert alert-info">
<h5><i class="bi bi-plus-slash-minus"></i> Use Plus Addressing</h5>
<p class="mb-0">Use email plus addressing (e.g., <code>yourname+github%40gmail.com</code>) to track which services are sending webhooks.</p>
</div>
<div class="alert alert-info">
<h5><i class="bi bi-clock-history"></i> Timestamp in Emails</h5>
<p class="mb-0">Check the "Date Received" field in emails to verify webhook timing and debug delivery delays.</p>
</div>
<div class="alert alert-info">
<h5><i class="bi bi-code"></i> Parse the Body</h5>
<p class="mb-0">The raw body section contains the exact payload - perfect for copying into your code for testing.</p>
</div>
</div>
<!-- Quick Start Section -->
<div class="doc-container">
<h2><i class="bi bi-rocket-takeoff"></i> Quick Start</h2>
<p class="lead">Get started in 3 simple steps:</p>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card text-center feature-card border-primary">
<div class="card-body">
<div class="display-4 text-primary">1</div>
<h5 class="card-title mt-3">Encode Your Email</h5>
<p class="card-text">Replace <code>@</code> with <code>%40</code> in your email address</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center feature-card border-success">
<div class="card-body">
<div class="display-4 text-success">2</div>
<h5 class="card-title mt-3">Configure Webhook</h5>
<p class="card-text">Use <code><?php echo $_SERVER['HTTP_HOST']; ?>/{your-email}</code> as the webhook URL</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-center feature-card border-warning">
<div class="card-body">
<div class="display-4 text-warning">3</div>
<h5 class="card-title mt-3">Check Your Inbox</h5>
<p class="card-text">Receive detailed webhook data instantly in your email</p>
</div>
</div>
</div>
</div>
<div class="text-center mt-4">
<h5>Try it now!</h5>
<div class="code-block text-start d-inline-block">
curl -X POST https://<?php echo $_SERVER['HTTP_HOST']; ?>/YOUR-EMAIL-HERE \
-d '{"test": "Hello World!"}'
</div>
</div>
</div>
<!-- Footer -->
<div class="text-center text-white mt-5">
<p class="mb-1"><i class="bi bi-code-slash"></i> Built with PHP and ADHD medication</p>
<p><small>© 2025-2026 Andy Dixon | <a href="https://<?php echo $_SERVER['HTTP_HOST']; ?>/" class="text-white"><?php echo $_SERVER['HTTP_HOST']; ?></a></small></p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>
+235 -338
View File
@@ -1,324 +1,204 @@
<?php <?php
/** /**
* Webhook Email Forwarder * Webhook Email Forwarder
* *
* This script captures incoming webhook requests and forwards the complete request details * This script captures incoming webhook requests and forwards the complete request details
* (headers, body, metadata) to an email address specified in the URL path. * (headers, body, metadata) to an email address specified in the URL path.
* *
* Usage: <?php echo $_SERVER['HTTP_HOST']; ?>/email%40domain.com * Usage: <?php echo $_SERVER['HTTP_HOST']; ?>/email%40domain.com
* *
* @author Dixon * @author Dixon
* @version 1.0 * @version 1.0
*/ */
// Prevent any output buffering issues // Prevent any output buffering issues
if (ob_get_level()) { if (ob_get_level()) {
ob_end_clean(); ob_end_clean();
} }
// Retrieve the complete request URI from the server superglobal // Retrieve the complete request URI from the server superglobal
$requestUri = $_SERVER['REQUEST_URI'] ?? ''; $requestUri = $_SERVER['REQUEST_URI'] ?? '';
// Parse the URL to extract only the path component, excluding any query strings // Parse the URL to extract only the path component, excluding any query strings
// This ensures that any GET parameters don't interfere with email extraction // This ensures that any GET parameters don't interfere with email extraction
$path = parse_url($requestUri, PHP_URL_PATH); $path = parse_url($requestUri, PHP_URL_PATH);
// Remove leading and trailing forward slashes from the path // Remove leading and trailing forward slashes from the path
// Example: '/email%40domain.com/' or '/email%40domain.com/github' // Example: '/email%40domain.com/' or '/email%40domain.com/github'
$pathParts = explode('/', trim($path, '/')); $pathParts = explode('/', trim($path, '/'));
// Extract email and optional parser // Extract email and optional parser
$emailEncoded = $pathParts[0] ?? ''; $emailEncoded = $pathParts[0] ?? '';
$parserName = $pathParts[1] ?? null; $parserName = $pathParts[1] ?? null;
// Decode the URL-encoded email address (e.g., %40 becomes @) // Decode the URL-encoded email address (e.g., %40 becomes @)
// Example: 'email%40domain.com' becomes 'email@domain.com' // Example: 'email%40domain.com' becomes 'email@domain.com'
$email = urldecode($emailEncoded); $email = urldecode($emailEncoded);
if(empty($email)) { if(empty($email)) {
include'docs.htm'; include'docs.htm';
die(); die();
} }
// Validate the extracted email address using PHP's built-in filter // Validate the extracted email address using PHP's built-in filter
// This ensures the email is in a valid format before proceeding // This ensures the email is in a valid format before proceeding
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Set HTTP response code to 400 Bad Request // Set HTTP response code to 400 Bad Request
http_response_code(400); http_response_code(400);
// Return error message to the client header('Content-Type: application/json; charset=UTF-8');
echo 'Invalid email address provided in URL path.'; echo json_encode([
// Terminate script execution 'ok' => false,
exit; 'error' => 'invalid_email',
} 'message' => 'Invalid email address provided in URL path.',
'hint' => 'Encode @ as %40, e.g. /you%40example.com',
// Capture the current date and time when the webhook request was received ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
// Format: YYYY-MM-DD HH:MM:SS (24-hour format) exit;
$dateReceived = date('Y-m-d H:i:s'); }
// Retrieve the IP address of the client making the request // Capture the current date and time when the webhook request was received
// Note: This may be a proxy IP if behind a load balancer or CDN // Format: YYYY-MM-DD HH:MM:SS (24-hour format)
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'Unknown'; $dateReceived = date('Y-m-d H:i:s');
// Capture the HTTP method used for this request (GET, POST, PUT, DELETE, etc.) // Retrieve the IP address of the client making the request
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'Unknown'; // Note: This may be a proxy IP if behind a load balancer or CDN
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
// Retrieve the Content-Type header sent by the client
// Use null coalescing operator to provide a default if not set // Capture the HTTP method used for this request (GET, POST, PUT, DELETE, etc.)
$contentType = $_SERVER['CONTENT_TYPE'] ?? 'N/A'; $requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'Unknown';
// Retrieve all HTTP headers sent with the request // Retrieve the Content-Type header sent by the client
// getallheaders() returns an associative array of all headers // Use null coalescing operator to provide a default if not set
$headersArray = getallheaders(); $contentType = $_SERVER['CONTENT_TYPE'] ?? 'N/A';
// Initialize an empty string to store formatted headers // Retrieve all HTTP headers sent with the request
$headersText = ''; // getallheaders() returns an associative array of all headers
$headersArray = getallheaders();
// Iterate through each header and format it as "Key: Value"
foreach ($headersArray as $key => $value) { // Initialize an empty string to store formatted headers
// Concatenate each header on a new line $headersText = '';
$headersText .= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') . ': ' .
htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . "\n"; // Iterate through each header and format it as "Key: Value"
} foreach ($headersArray as $key => $value) {
// Concatenate each header on a new line
// Read the raw request body from the input stream $headersText .= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') . ': ' .
// php://input allows access to the raw POST data regardless of Content-Type htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . "\n";
$rawBody = file_get_contents('php://input'); }
// Sanitise the raw body for HTML output to prevent XSS attacks // Read the raw request body from the input stream
$rawBodySafe = htmlspecialchars($rawBody, ENT_QUOTES, 'UTF-8'); // php://input allows access to the raw POST data regardless of Content-Type
$rawBody = file_get_contents('php://input');
// Compile all PHP superglobal variables into an array for debugging purposes
// This provides visibility of all data received with the request // Sanitise the raw body for HTML output to prevent XSS attacks
$variables = [ $rawBodySafe = htmlspecialchars($rawBody, ENT_QUOTES, 'UTF-8');
'GET Variables' => $_GET,
'POST Variables' => $_POST, // Compile all PHP superglobal variables into an array for debugging purposes
'Raw Request Body' => $_REQUEST, // This provides visibility of all data received with the request
'Uploaded Files' => $_FILES, $variables = [
]; 'GET Variables' => $_GET,
'POST Variables' => $_POST,
// Convert the variables array to a human-readable string format 'Raw Request Body' => $_REQUEST,
// The second parameter (true) makes print_r return the output instead of printing it 'Uploaded Files' => $_FILES,
$variablesText = print_r($variables, true); ];
// Sanitise the variables text for HTML output // Convert the variables array to a human-readable string format
$variablesTextSafe = htmlspecialchars($variablesText, ENT_QUOTES, 'UTF-8'); // The second parameter (true) makes print_r return the output instead of printing it
$variablesText = json_encode($variables, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Sanitise individual metadata fields for HTML output
$dateReceivedSafe = htmlspecialchars($dateReceived, ENT_QUOTES, 'UTF-8'); // Sanitise the variables text for HTML output
$ipAddressSafe = htmlspecialchars($ipAddress, ENT_QUOTES, 'UTF-8'); $variablesTextSafe = htmlspecialchars($variablesText, ENT_QUOTES, 'UTF-8');
$requestMethodSafe = htmlspecialchars($requestMethod, ENT_QUOTES, 'UTF-8');
$contentTypeSafe = htmlspecialchars($contentType, ENT_QUOTES, 'UTF-8'); // Sanitise individual metadata fields for HTML output
$dateReceivedSafe = htmlspecialchars($dateReceived, ENT_QUOTES, 'UTF-8');
// Load parser helpers $ipAddressSafe = htmlspecialchars($ipAddress, ENT_QUOTES, 'UTF-8');
require_once __DIR__ . '/parsers/helpers.php'; $requestMethodSafe = htmlspecialchars($requestMethod, ENT_QUOTES, 'UTF-8');
$contentTypeSafe = htmlspecialchars($contentType, ENT_QUOTES, 'UTF-8');
// Check if a parser is specified and load it
$html = ''; // Load parser helpers
$subject = ''; require_once __DIR__ . '/parsers/helpers.php';
if ($parserName) {
$parserFile = __DIR__ . '/parsers/' . $parserName . '.php'; // Check if a parser is specified and load it
if (file_exists($parserFile)) { $html = '';
require_once $parserFile; $subject = '';
$parserFunction = $parserName . 'Parse'; if ($parserName) {
$parserFile = __DIR__ . '/parsers/' . $parserName . '.php';
if (function_exists($parserFunction)) { if (file_exists($parserFile)) {
$parsed = $parserFunction($rawBody, $headersArray, [ require_once $parserFile;
'date' => $dateReceived, $parserFunction = $parserName . 'Parse';
'ip' => $ipAddress,
'method' => $requestMethod, if (function_exists($parserFunction)) {
'contentType' => $contentType $parsed = $parserFunction($rawBody, $headersArray, [
]); 'date' => $dateReceived,
'ip' => $ipAddress,
if ($parsed !== false) { 'method' => $requestMethod,
$html = $parsed['html']; 'contentType' => $contentType
$subject = $parsed['subject']; ]);
}
} if ($parsed !== false) {
} $html = $parsed['html'];
} $subject = $parsed['subject'];
}
// If no parser or parser failed, use default format }
if (empty($html)) { }
// Construct the HTML email body using a heredoc string for better readability }
// This creates a styled HTML email with all webhook details
$html = <<<HTML // If no parser or parser failed, use default format
<!DOCTYPE html> if (empty($html)) {
<html lang="en-GB"> // Construct the HTML email body using a heredoc string for better readability
<head> // This creates a styled HTML email with all webhook details
<meta charset="UTF-8"> $emailTitle = "⚡ Webhook Request Received";
<meta name="viewport" content="width=device-width, initial-scale=1.0"> $emailStamp = "$dateReceivedSafe";
<title>Webhook Request Received</title> $body = <<<HTML
<style>
/* Gmail-safe reset and main styling */ <!-- Request Information Section -->
body { <div class="section">
background-color: #ffffff; <div class="section-title">📋 Request Information</div>
color: #1a1a1a; <div class="metadata">
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; <div class="metadata-row">
line-height: 1.6; <div class="metadata-label">IP Address</div>
padding: 0; <div class="metadata-value">$ipAddressSafe</div>
margin: 0; </div>
} <div class="metadata-row">
/* Container for better email client compatibility */ <div class="metadata-label">Method</div>
.container { <div class="metadata-value">$requestMethodSafe</div>
max-width: 700px; </div>
margin: 0 auto; <div class="metadata-row">
padding: 40px 20px; <div class="metadata-label">Content-Type</div>
} <div class="metadata-value">$contentTypeSafe</div>
/* Header styling with elegant separator */ </div>
.header { </div>
text-align: center; </div>
padding-bottom: 30px;
border-bottom: 3px solid #000000; <!-- Request Headers Section -->
margin-bottom: 40px; <div class="section">
} <div class="section-title">📨 Request Headers</div>
.header h1 { <pre class="data-box">$headersText</pre>
color: #000000; </div>
font-size: 28px;
font-weight: 700; <!-- Request Body Section -->
margin: 0 0 10px 0; <div class="section">
letter-spacing: -0.5px; <div class="section-title">📦 Request Body</div>
} <pre class="data-box">$rawBodySafe</pre>
.header .timestamp { </div>
color: #666666;
font-size: 14px; <!-- PHP Variables Section -->
font-weight: 500; <div class="section">
margin: 0; <div class="section-title">🔧 Parsed Variables</div>
} <pre class="data-box">$variablesTextSafe</pre>
/* Section styling with elegant typography */ </div>
.section {
margin-bottom: 35px; <!-- Footer -->
} HTML;
.section-title { $html = emailShell($emailTitle, $emailStamp, $body, "This webhook was automatically forwarded to your email address.");
color: #000000;
font-size: 18px; // Construct the email subject line with an attention-grabbing emoji and timestamp
font-weight: 700; $subject = "‼️ Webhook Request Received - $dateReceived";
margin: 0 0 12px 0; }
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0; // Build email headers to ensure proper HTML rendering and sender information
letter-spacing: -0.3px; // MIME-Version declares email format capabilities
} $emailHeaders = "MIME-Version: 1.0\r\n";
/* Data display with subtle borders */ // Content-type specifies HTML email with UTF-8 character encoding
.data-box { $emailHeaders .= "Content-type: text/html; charset=UTF-8\r\n";
border: 1px solid #d0d0d0;
border-radius: 8px;
padding: 16px 20px;
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
color: #2a2a2a;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
/* Metadata grid for clean information display */
.metadata {
display: table;
width: 100%;
border: 1px solid #d0d0d0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.metadata-row {
display: table-row;
}
.metadata-row:not(:last-child) .metadata-label,
.metadata-row:not(:last-child) .metadata-value {
border-bottom: 1px solid #e8e8e8;
}
.metadata-label {
display: table-cell;
padding: 12px 16px;
font-weight: 600;
color: #000000;
width: 35%;
vertical-align: top;
font-size: 14px;
}
.metadata-value {
display: table-cell;
padding: 12px 16px;
color: #333333;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
vertical-align: top;
}
/* Footer styling */
.footer {
margin-top: 50px;
padding-top: 25px;
border-top: 2px solid #e0e0e0;
text-align: center;
color: #888888;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<!-- Email Header -->
<div class="header">
<h1>⚡ Webhook Request Received</h1>
<p class="timestamp">$dateReceivedSafe</p>
</div>
<!-- Request Information Section -->
<div class="section">
<div class="section-title">📋 Request Information</div>
<div class="metadata">
<div class="metadata-row">
<div class="metadata-label">IP Address</div>
<div class="metadata-value">$ipAddressSafe</div>
</div>
<div class="metadata-row">
<div class="metadata-label">Method</div>
<div class="metadata-value">$requestMethodSafe</div>
</div>
<div class="metadata-row">
<div class="metadata-label">Content-Type</div>
<div class="metadata-value">$contentTypeSafe</div>
</div>
</div>
</div>
<!-- Request Headers Section -->
<div class="section">
<div class="section-title">📨 Request Headers</div>
<pre class="data-box">$headersText</pre>
</div>
<!-- Request Body Section -->
<div class="section">
<div class="section-title">📦 Request Body</div>
<pre class="data-box">$rawBodySafe</pre>
</div>
<!-- PHP Variables Section -->
<div class="section">
<div class="section-title">🔧 PHP Variables</div>
<pre class="data-box">$variablesTextSafe</pre>
</div>
<!-- Footer -->
<div class="footer">
<p>This webhook was automatically forwarded to your email address.</p>
</div>
</div>
</body>
</html>
HTML;
// Construct the email subject line with an attention-grabbing emoji and timestamp
$subject = "‼️ Webhook Request Received - $dateReceived";
}
// Build email headers to ensure proper HTML rendering and sender information
// MIME-Version declares email format capabilities
$emailHeaders = "MIME-Version: 1.0\r\n";
// Content-type specifies HTML email with UTF-8 character encoding
$emailHeaders .= "Content-type: text/html; charset=UTF-8\r\n";
// Keep both the visible sender and SMTP envelope sender on the public domain. // Keep both the visible sender and SMTP envelope sender on the public domain.
$envelopeSender = 'no-reply@dixon.cx'; $envelopeSender = 'no-reply@dixon.cx';
$emailHeaders .= "From: Webhook Call <$envelopeSender>\r\n"; $emailHeaders .= "From: Webhook Call <$envelopeSender>\r\n";
@@ -326,20 +206,37 @@ $emailHeaders .= "From: Webhook Call <$envelopeSender>\r\n";
// Send the email using PHP's built-in mail function // Send the email using PHP's built-in mail function
// The -f option prevents sendmail from deriving www-data@<container-hostname>. // The -f option prevents sendmail from deriving www-data@<container-hostname>.
$mailSent = mail($email, $subject, $html, $emailHeaders, '-f' . $envelopeSender); $mailSent = mail($email, $subject, $html, $emailHeaders, '-f' . $envelopeSender);
// Check if the email was sent successfully // Check if the email was sent successfully
if (!$mailSent) { if (!$mailSent) {
// Log error or handle failure (note: mail() return value is unreliable on some systems) // Log error or handle failure (note: mail() return value is unreliable on some systems)
error_log("Failed to send webhook email to: $email"); error_log("Failed to send webhook email to: $email");
} }
// Set the Content-Type header for the HTTP response to plain text // JSON acknowledgement back to the webhook sender
header('Content-Type: text/plain; charset=UTF-8'); header('Content-Type: application/json; charset=UTF-8');
// Return a simple acknowledgement to the webhook sender // Decode JSON bodies so the caller sees what we understood, not a raw string
// This confirms the webhook was received and processed $bodyJson = ($rawBody !== '' && str_contains($contentType, 'json')) ? json_decode($rawBody, true) : null;
echo "Webhook received and forwarded to: $email\n\n";
echo json_encode([
// Include a sanitised copy of the REQUEST data in the response for debugging 'ok' => true,
echo "Request data:\n"; 'message' => "Webhook received and forwarded to $email",
print_r($_REQUEST); 'forwarded_to' => $email,
'parser' => $parserName ?: 'default',
'subject' => $subject,
'received_at' => date('c'),
'request' => [
'method' => $requestMethod,
'content_type' => $contentType,
'ip' => $ipAddress,
'headers' => count($headersArray),
'body_bytes' => strlen($rawBody),
],
'data' => [
'query' => (object) $_GET,
'form' => (object) $_POST,
'json' => $bodyJson,
'files' => array_keys($_FILES),
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
+7 -181
View File
@@ -106,7 +106,7 @@ HTML;
</div> </div>
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">URL</div> <div class="metadata-label">URL</div>
<div class="metadata-value"><a href="$prUrl" style="color: #0066cc;">$prUrl</a></div> <div class="metadata-value"><a href="$prUrl" style="color: #7c3aed;">$prUrl</a></div>
</div> </div>
</div> </div>
</div> </div>
@@ -139,7 +139,7 @@ HTML;
</div> </div>
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">URL</div> <div class="metadata-label">URL</div>
<div class="metadata-value"><a href="$issueUrl" style="color: #0066cc;">$issueUrl</a></div> <div class="metadata-value"><a href="$issueUrl" style="color: #7c3aed;">$issueUrl</a></div>
</div> </div>
</div> </div>
</div> </div>
@@ -167,7 +167,7 @@ HTML;
</div> </div>
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">URL</div> <div class="metadata-label">URL</div>
<div class="metadata-value"><a href="$releaseUrl" style="color: #0066cc;">$releaseUrl</a></div> <div class="metadata-value"><a href="$releaseUrl" style="color: #7c3aed;">$releaseUrl</a></div>
</div> </div>
</div> </div>
</div> </div>
@@ -183,177 +183,9 @@ HTML;
$actionDisplay = $actionSafe ? " ($actionSafe)" : ''; $actionDisplay = $actionSafe ? " ($actionSafe)" : '';
$html = <<<HTML $emailTitle = "$eventIcon GitHub: $eventSafe$actionDisplay";
<!DOCTYPE html> $emailStamp = "$dateSafe";
<html lang="en-GB"> $body = <<<HTML
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub Webhook - $eventSafe</title>
<style>
body {
background-color: #ffffff;
color: #1a1a1a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
padding: 0;
margin: 0;
}
.container {
max-width: 700px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
text-align: center;
padding-bottom: 30px;
border-bottom: 3px solid #000000;
margin-bottom: 40px;
}
.header h1 {
color: #000000;
font-size: 28px;
font-weight: 700;
margin: 0 0 10px 0;
letter-spacing: -0.5px;
}
.header .timestamp {
color: #666666;
font-size: 14px;
font-weight: 500;
margin: 0;
}
.section {
margin-bottom: 35px;
}
.section-title {
color: #000000;
font-size: 18px;
font-weight: 700;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0;
letter-spacing: -0.3px;
}
.data-box {
border: 1px solid #d0d0d0;
border-radius: 8px;
padding: 16px 20px;
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
color: #2a2a2a;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.metadata {
display: table;
width: 100%;
border: 1px solid #d0d0d0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.metadata-row {
display: table-row;
}
.metadata-row:not(:last-child) .metadata-label,
.metadata-row:not(:last-child) .metadata-value {
border-bottom: 1px solid #e8e8e8;
}
.metadata-label {
display: table-cell;
padding: 12px 16px;
font-weight: 600;
color: #000000;
width: 35%;
vertical-align: top;
font-size: 14px;
}
.metadata-value {
display: table-cell;
padding: 12px 16px;
color: #333333;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
vertical-align: top;
}
.commit-item {
padding: 10px 16px;
border-left: 3px solid #e0e0e0;
margin-bottom: 8px;
background: #f8f8f8;
border-radius: 4px;
font-size: 14px;
}
.commit-item strong {
color: #0066cc;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
}
.commit-item em {
color: #666666;
font-style: normal;
font-size: 13px;
}
.subsection {
margin: 20px 0;
}
.subsection-title {
color: #000000;
font-size: 16px;
font-weight: 600;
margin: 15px 0 10px 0;
padding: 8px 12px;
background: #f0f0f0;
border-left: 4px solid #666666;
border-radius: 4px;
}
.array-item {
margin-bottom: 20px;
padding: 12px;
background: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
.array-item-title {
font-weight: 600;
color: #000000;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid #d0d0d0;
}
.array-item-simple {
padding: 8px 12px;
margin: 4px 0;
background: #f8f8f8;
border-left: 3px solid #d0d0d0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
}
.simple-value {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
color: #333333;
}
.footer {
margin-top: 50px;
padding-top: 25px;
border-top: 2px solid #e0e0e0;
text-align: center;
color: #888888;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>$eventIcon GitHub: $eventSafe$actionDisplay</h1>
<p class="timestamp">$dateSafe</p>
</div>
<div class="section"> <div class="section">
<div class="section-title">📋 Repository Information</div> <div class="section-title">📋 Repository Information</div>
@@ -378,14 +210,8 @@ HTML;
</div> </div>
$eventContent $eventContent
<div class="footer">
<p>This GitHub webhook was automatically forwarded to your email address.</p>
</div>
</div>
</body>
</html>
HTML; HTML;
$html = emailShell($emailTitle, $emailStamp, $body, "This GitHub webhook was automatically forwarded to your email address.");
$subject = "$eventIcon GitHub $eventSafe$actionDisplay - $repoSafe"; $subject = "$eventIcon GitHub $eventSafe$actionDisplay - $repoSafe";
+6 -135
View File
@@ -76,7 +76,7 @@ HTML;
$alertSummary .= <<<HTML $alertSummary .= <<<HTML
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">Alertmanager URL</div> <div class="metadata-label">Alertmanager URL</div>
<div class="metadata-value"><a href="$externalURLSafe" style="color: #0066cc;">$externalURLSafe</a></div> <div class="metadata-value"><a href="$externalURLSafe" style="color: #7c3aed;">$externalURLSafe</a></div>
</div> </div>
HTML; HTML;
} }
@@ -233,7 +233,7 @@ HTML;
$alertsContent .= <<<HTML $alertsContent .= <<<HTML
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">Generator URL</div> <div class="metadata-label">Generator URL</div>
<div class="metadata-value"><a href="$generatorURLSafe" style="color: #0066cc;">$generatorURLSafe</a></div> <div class="metadata-value"><a href="$generatorURLSafe" style="color: #7c3aed;">$generatorURLSafe</a></div>
</div> </div>
HTML; HTML;
} }
@@ -254,132 +254,9 @@ HTML;
$alertsContent .= '</div>'; $alertsContent .= '</div>';
} }
$html = <<<HTML $emailTitle = "$statusIcon Grafana Alert: $statusSafe";
<!DOCTYPE html> $emailStamp = "$dateSafe";
<html lang="en-GB"> $body = <<<HTML
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grafana Alert - $statusSafe</title>
<style>
body {
background-color: #ffffff;
color: #1a1a1a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
padding: 0;
margin: 0;
}
.container {
max-width: 700px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
text-align: center;
padding-bottom: 30px;
border-bottom: 3px solid #000000;
margin-bottom: 40px;
}
.header h1 {
color: #000000;
font-size: 28px;
font-weight: 700;
margin: 0 0 10px 0;
letter-spacing: -0.5px;
}
.header .timestamp {
color: #666666;
font-size: 14px;
font-weight: 500;
margin: 0;
}
.section {
margin-bottom: 35px;
}
.section-title {
color: #000000;
font-size: 18px;
font-weight: 700;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0;
letter-spacing: -0.3px;
}
.metadata {
display: table;
width: 100%;
border: 1px solid #d0d0d0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.metadata-row {
display: table-row;
}
.metadata-row:not(:last-child) .metadata-label,
.metadata-row:not(:last-child) .metadata-value {
border-bottom: 1px solid #e8e8e8;
}
.metadata-label {
display: table-cell;
padding: 12px 16px;
font-weight: 600;
color: #000000;
width: 35%;
vertical-align: top;
font-size: 14px;
}
.metadata-value {
display: table-cell;
padding: 12px 16px;
color: #333333;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
vertical-align: top;
word-wrap: break-word;
}
.subsection-title {
color: #000000;
font-size: 16px;
font-weight: 600;
margin: 15px 0 10px 0;
padding: 8px 12px;
background: #f0f0f0;
border-left: 4px solid #666666;
border-radius: 4px;
}
.array-item {
margin-bottom: 20px;
padding: 12px;
background: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
.array-item-title {
font-weight: 600;
color: #000000;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid #d0d0d0;
font-size: 16px;
}
.footer {
margin-top: 50px;
padding-top: 25px;
border-top: 2px solid #e0e0e0;
text-align: center;
color: #888888;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>$statusIcon Grafana Alert: $statusSafe</h1>
<p class="timestamp">$dateSafe</p>
</div>
$alertSummary $alertSummary
@@ -402,14 +279,8 @@ HTML;
</div> </div>
</div> </div>
</div> </div>
<div class="footer">
<p>This Grafana/Prometheus alert was automatically forwarded to your email address.</p>
</div>
</div>
</body>
</html>
HTML; HTML;
$html = emailShell($emailTitle, $emailStamp, $body, "This Grafana/Prometheus alert was automatically forwarded to your email address.");
// Create subject line // Create subject line
$alertCountText = $alertCount === 1 ? '1 alert' : "$alertCount alerts"; $alertCountText = $alertCount === 1 ? '1 alert' : "$alertCount alerts";
+75
View File
@@ -89,3 +89,78 @@ function renderDataAsTable($data, $depth = 0) {
return $html; return $html;
} }
/**
* Shared email chrome: one stylesheet, one header, one footer, for every template.
*
* @param string $title Header text (already HTML-safe)
* @param string $stamp Timestamp line (already HTML-safe)
* @param string $body Inner HTML made of .section / .metadata / .data-box blocks
* @param string $footer Footer sentence
* @return string Complete HTML document
*/
function emailShell($title, $stamp, $body, $footer = 'Automatically forwarded to your inbox.') {
$host = htmlspecialchars($_SERVER['HTTP_HOST'] ?? 'webhook', ENT_QUOTES, 'UTF-8');
$plainTitle = strip_tags($title);
return <<<HTML
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light">
<title>$plainTitle</title>
<style>
body { margin:0; padding:0; background:#f3f1fb; color:#15151f; -webkit-font-smoothing:antialiased;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; font-size:15px; line-height:1.6; }
a { color:#7c3aed; }
.wrap { padding:28px 12px; }
.container { max-width:640px; margin:0 auto; background:#ffffff; border-radius:16px; overflow:hidden;
box-shadow:0 12px 40px rgba(40,20,90,.10); border:1px solid #e9e6f5; }
.stripe { height:6px; background:#8b5cf6; background-image:linear-gradient(90deg,#8b5cf6,#ec4899 55%,#f97316); }
.header { padding:28px 32px 20px; border-bottom:1px solid #eeecf6; }
.header h1 { margin:0 0 6px; font-size:22px; font-weight:800; letter-spacing:-.02em; line-height:1.25; color:#15151f; }
.timestamp { margin:0; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; color:#8b8ba3; letter-spacing:.06em; text-transform:uppercase; }
.section { padding:22px 32px 0; }
.section-title { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:#7c3aed; margin:0 0 10px; }
.subsection { margin:14px 0; }
.subsection-title { font-size:13px; font-weight:700; color:#15151f; margin:16px 0 8px; padding-left:10px; border-left:3px solid #ec4899; }
.metadata { display:table; width:100%; border:1px solid #e9e6f5; border-radius:10px; border-collapse:separate; overflow:hidden; }
.metadata-row { display:table-row; }
.metadata-label, .metadata-value { display:table-cell; padding:10px 14px; border-bottom:1px solid #f0eef8; vertical-align:top; }
.metadata-row:last-child .metadata-label, .metadata-row:last-child .metadata-value { border-bottom:0; }
.metadata-label { width:150px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; color:#6b6b80; background:#faf9fe; white-space:nowrap; }
.metadata-value { font-size:14px; color:#15151f; word-break:break-word; }
.data-box { margin:0; padding:16px 18px; background:#0f0f17; color:#e6e6f0; border-radius:10px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
font-size:12.5px; line-height:1.65; white-space:pre-wrap; word-break:break-word; overflow-x:auto; }
.path-title { margin:18px 0 8px; padding:8px 12px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; font-weight:700;
color:#db2777; background:#fdf2f8; border-left:3px solid #ec4899; border-radius:0 8px 8px 0; }
.array-item, .array-item-simple { margin:0 0 12px; padding:14px 16px; background:#faf9fe; border:1px solid #e9e6f5; border-radius:10px; }
.array-item-title { font-size:13px; font-weight:700; margin:0 0 10px; color:#15151f; }
.array-item .metadata { background:#fff; }
.message-box { margin:12px 0; padding:14px 16px; background:#faf9fe; border:1px solid #e9e6f5; border-radius:10px; font-size:15px; line-height:1.6; }
.error-box { margin:12px 0; padding:12px 14px; background:#fef2f2; border-left:3px solid #ef4444; border-radius:0 8px 8px 0; }
.footer { margin-top:28px; padding:18px 32px 24px; border-top:1px solid #eeecf6; text-align:center; color:#8b8ba3;
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; letter-spacing:.04em; }
.footer p { margin:0 0 4px; }
</style>
</head>
<body>
<div class="wrap">
<div class="container">
<div class="stripe"></div>
<div class="header">
<h1>$title</h1>
<p class="timestamp">$stamp</p>
</div>
$body
<div class="footer">
<p>$footer</p>
<p>⚡ $host</p>
</div>
</div>
</div>
</body>
</html>
HTML;
}
+6 -135
View File
@@ -22,132 +22,9 @@ function jsonParse($rawBody, $headers, $metadata) {
$contentTypeSafe = htmlspecialchars($metadata['contentType'], ENT_QUOTES, 'UTF-8'); $contentTypeSafe = htmlspecialchars($metadata['contentType'], ENT_QUOTES, 'UTF-8');
// Start building HTML // Start building HTML
$html = <<<HTML $emailTitle = "📊 JSON Webhook Data";
<!DOCTYPE html> $emailStamp = "$dateSafe";
<html lang="en-GB"> $body = <<<HTML
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSON Webhook Data</title>
<style>
body {
background-color: #ffffff;
color: #1a1a1a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
padding: 0;
margin: 0;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
text-align: center;
padding-bottom: 30px;
border-bottom: 3px solid #000000;
margin-bottom: 40px;
}
.header h1 {
color: #000000;
font-size: 28px;
font-weight: 700;
margin: 0 0 10px 0;
letter-spacing: -0.5px;
}
.header .timestamp {
color: #666666;
font-size: 14px;
font-weight: 500;
margin: 0;
}
.section {
margin-bottom: 35px;
}
.section-title {
color: #000000;
font-size: 18px;
font-weight: 700;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0;
letter-spacing: -0.3px;
}
.path-title {
color: #0066cc;
font-size: 16px;
font-weight: 600;
margin: 25px 0 12px 0;
padding: 8px 12px;
background-color: #f0f7ff;
border-left: 4px solid #0066cc;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
}
.metadata {
display: table;
width: 100%;
border: 1px solid #d0d0d0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
margin-bottom: 15px;
}
.metadata-row {
display: table-row;
}
.metadata-row:not(:last-child) .metadata-label,
.metadata-row:not(:last-child) .metadata-value {
border-bottom: 1px solid #e8e8e8;
}
.metadata-label {
display: table-cell;
padding: 12px 16px;
font-weight: 600;
color: #000000;
width: 30%;
vertical-align: top;
font-size: 14px;
background-color: #f9f9f9;
}
.metadata-value {
display: table-cell;
padding: 12px 16px;
color: #333333;
font-size: 13px;
vertical-align: top;
word-break: break-word;
}
.array-item {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background-color: #fafafa;
}
.array-item-title {
font-weight: 700;
color: #000000;
margin-bottom: 10px;
font-size: 14px;
}
.footer {
margin-top: 50px;
padding-top: 25px;
border-top: 2px solid #e0e0e0;
text-align: center;
color: #888888;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 JSON Webhook Data</h1>
<p class="timestamp">$dateSafe</p>
</div>
<div class="section"> <div class="section">
<div class="section-title">📋 Request Information</div> <div class="section-title">📋 Request Information</div>
@@ -173,18 +50,12 @@ function jsonParse($rawBody, $headers, $metadata) {
HTML; HTML;
// Process the JSON data // Process the JSON data
$html .= renderJsonData($payload); $body .= renderJsonData($payload);
$html .= <<<HTML $body .= <<<HTML
</div> </div>
<div class="footer">
<p>JSON data automatically parsed and formatted.</p>
</div>
</div>
</body>
</html>
HTML; HTML;
$html = emailShell($emailTitle, $emailStamp, $body, "JSON data automatically parsed and formatted.");
$subject = "📊 JSON Webhook - " . $metadata['date']; $subject = "📊 JSON Webhook - " . $metadata['date'];
+6 -153
View File
@@ -82,150 +82,9 @@ function wxinteractParse($rawBody, $headers, $metadata) {
$eventTitleSafe = htmlspecialchars($eventTitle, ENT_QUOTES, 'UTF-8'); $eventTitleSafe = htmlspecialchars($eventTitle, ENT_QUOTES, 'UTF-8');
$html = <<<HTML $emailTitle = "$eventIcon Webex Interact: $eventTitleSafe";
<!DOCTYPE html> $emailStamp = "$dateSafe";
<html lang="en-GB"> $body = <<<HTML
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webex Interact - $eventTitleSafe</title>
<style>
body {
background-color: #ffffff;
color: #1a1a1a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
padding: 0;
margin: 0;
}
.container {
max-width: 700px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
text-align: center;
padding-bottom: 30px;
border-bottom: 3px solid #000000;
margin-bottom: 40px;
}
.header h1 {
color: #000000;
font-size: 28px;
font-weight: 700;
margin: 0 0 10px 0;
letter-spacing: -0.5px;
}
.header .timestamp {
color: #666666;
font-size: 14px;
font-weight: 500;
margin: 0;
}
.section {
margin-bottom: 35px;
}
.section-title {
color: #000000;
font-size: 18px;
font-weight: 700;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 2px solid #e0e0e0;
letter-spacing: -0.3px;
}
.data-box {
border: 1px solid #d0d0d0;
border-radius: 8px;
padding: 16px 20px;
margin: 10px 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
color: #2a2a2a;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
background: #f8f8f8;
}
.metadata {
display: table;
width: 100%;
border: 1px solid #d0d0d0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.metadata-row {
display: table-row;
}
.metadata-row:not(:last-child) .metadata-label,
.metadata-row:not(:last-child) .metadata-value {
border-bottom: 1px solid #e8e8e8;
}
.metadata-label {
display: table-cell;
padding: 12px 16px;
font-weight: 600;
color: #000000;
width: 35%;
vertical-align: top;
font-size: 14px;
}
.metadata-value {
display: table-cell;
padding: 12px 16px;
color: #333333;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
vertical-align: top;
}
.message-box {
border-left: 4px solid #0066cc;
background: #f8f8f8;
padding: 16px 20px;
margin: 15px 0;
border-radius: 4px;
font-size: 15px;
line-height: 1.6;
}
.error-box {
border-left: 4px solid #cc0000;
background: #fff5f5;
padding: 16px 20px;
margin: 15px 0;
border-radius: 4px;
}
.subsection {
margin: 20px 0;
}
.subsection-title {
color: #000000;
font-size: 16px;
font-weight: 600;
margin: 15px 0 10px 0;
padding: 8px 12px;
background: #f0f0f0;
border-left: 4px solid #666666;
border-radius: 4px;
}
.footer {
margin-top: 50px;
padding-top: 25px;
border-top: 2px solid #e0e0e0;
text-align: center;
color: #888888;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>$eventIcon Webex Interact: $eventTitleSafe</h1>
<p class="timestamp">$dateSafe</p>
</div>
<div class="section"> <div class="section">
<div class="section-title">📋 Event Information</div> <div class="section-title">📋 Event Information</div>
@@ -242,14 +101,8 @@ function wxinteractParse($rawBody, $headers, $metadata) {
</div> </div>
$eventContent $eventContent
<div class="footer">
<p>This Webex Interact webhook was automatically forwarded to your email address.</p>
</div>
</div>
</body>
</html>
HTML; HTML;
$html = emailShell($emailTitle, $emailStamp, $body, "This Webex Interact webhook was automatically forwarded to your email address.");
$subject = "$eventIcon Webex Interact: $eventTitle"; $subject = "$eventIcon Webex Interact: $eventTitle";
@@ -411,11 +264,11 @@ function buildShortlinkClickedContent($data) {
<div class="metadata"> <div class="metadata">
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">Shortlink</div> <div class="metadata-label">Shortlink</div>
<div class="metadata-value"><a href="$shortlink" style="color: #0066cc;">$shortlink</a></div> <div class="metadata-value"><a href="$shortlink" style="color: #7c3aed;">$shortlink</a></div>
</div> </div>
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">Original URL</div> <div class="metadata-label">Original URL</div>
<div class="metadata-value"><a href="$originalUrl" style="color: #0066cc;">$originalUrl</a></div> <div class="metadata-value"><a href="$originalUrl" style="color: #7c3aed;">$originalUrl</a></div>
</div> </div>
<div class="metadata-row"> <div class="metadata-row">
<div class="metadata-label">Phone Number</div> <div class="metadata-label">Phone Number</div>