diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..e32a375
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+*
+!go.mod
+!*.go
+!docs.html
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..10253ca
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+webhooks
diff --git a/.htaccess b/.htaccess
deleted file mode 100755
index 8594ad8..0000000
--- a/.htaccess
+++ /dev/null
@@ -1,8 +0,0 @@
-RewriteEngine On
-
-# Skip existing files and directories
-RewriteCond %{REQUEST_FILENAME} !-f
-RewriteCond %{REQUEST_FILENAME} !-d
-
-# Redirect all requests to the PHP script
-RewriteRule ^(.*)$ index.php [L,QSA]
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ca3962c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM golang:1.26-alpine AS build
+WORKDIR /src
+COPY go.mod ./
+COPY *.go docs.html ./
+RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /webhooks .
+
+FROM gcr.io/distroless/static-debian12:nonroot
+COPY --from=build /webhooks /webhooks
+ENV LISTEN=:8080 SMTP_ADDR=172.17.0.1:25 MAIL_FROM=no-reply@dixon.cx PUBLIC_HOST=webhooks.dixon.cx
+EXPOSE 8080
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s CMD ["/webhooks", "-check"]
+ENTRYPOINT ["/webhooks"]
diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md
deleted file mode 100644
index ce095ea..0000000
--- a/IMPLEMENTATION_SUMMARY.md
+++ /dev/null
@@ -1,249 +0,0 @@
-# Webex Interact Parser Implementation Summary
-
-## โ
Implementation Complete
-
-The Webex Interact webhook parser has been successfully implemented and integrated into the webhook email forwarder system.
-
-## ๐ฆ What Was Added
-
-### 1. Main Parser Function (`index.php`)
-- **Function:** `wxinteractParse($rawBody, $headers, $metadata)`
-- **Location:** Line 799 in `index.php`
-- **Returns:** Array with 'html' and 'subject' keys, or false on failure
-
-### 2. Helper Functions (7 total)
-Each event type has a dedicated helper function for clean, maintainable code:
-
-1. `buildOutboundSmsSubmittedContent($data)` - ๐ค Submitted SMS
-2. `buildOutboundSmsDeliveredContent($data)` - โ
Delivered SMS
-3. `buildOutboundSmsFailedContent($data)` - โ Failed SMS
-4. `buildShortlinkClickedContent($data)` - ๐ Link clicks
-5. `buildInboundSmsContent($data)` - ๐ฅ Inbound messages
-6. `buildOptOutContent($data)` - ๐ซ Opt-out requests
-7. `buildContactsCallbackContent($data)` - ๐ค Contact updates
-
-### 3. Documentation Files
-
-#### `WEBEX_INTERACT_GUIDE.md`
-Comprehensive guide including:
-- Configuration instructions
-- All 7 event types with examples
-- Testing instructions
-- Troubleshooting tips
-- API documentation links
-
-#### `wxinteract_test_samples.json`
-Complete test payloads for all event types:
-- outbound_sms_submitted
-- outbound_sms_delivered
-- outbound_sms_failed
-- shortlink_clicked
-- inbound_sms_received
-- sms_opt_out
-- contact_created
-
-#### Updated `README.md`
-Added Webex Interact section with:
-- Parser overview
-- Supported events list
-- Usage examples
-- Email format samples
-
-## ๐ฏ Supported Events
-
-All 7 Webex Interact callback types are supported:
-
-| Event Type | Status Value | Icon | Description |
-|------------|-------------|------|-------------|
-| Outbound SMS - Submitted | `submitted` | ๐ค | Message submitted to carrier |
-| Outbound SMS - Delivered | `delivered` | โ
| Message delivered successfully |
-| Outbound SMS - Failed | `failed` | โ | Message failed to deliver |
-| Shortlink Clicked | `shortlink_clicked` | ๐ | Recipient clicked a tracked link |
-| Inbound SMS Received | `received` | ๐ฅ | Inbound message received |
-| SMS Opt Out Received | `opt_out` | ๐ซ | Recipient opted out |
-| Contacts Callback | `contact_created` / `contact_updated` | ๐ค | Contact created or updated |
-
-## ๐ง How It Works
-
-### Event Detection
-The parser identifies the event type using the `data.status` field:
-
-```json
-{
- "data": {
- "status": "submitted", // โ Parser reads this
- "message_id": "...",
- ...
- }
-}
-```
-
-### URL Format
-```
-https://your-domain.com/{email}/wxinteract
- ^^^^^^^^^ parser trigger
-```
-
-### Processing Flow
-1. Webhook received at URL with `/wxinteract`
-2. `index.php` checks if `wxinteractParse()` function exists โ
-3. Function parses JSON and extracts `data.status`
-4. Routes to appropriate helper function based on status
-5. Helper function builds formatted HTML
-6. Email sent with clean, readable format
-
-## ๐จ Design Features
-
-### Consistent Styling
-- Uses same CSS framework as GitHub parser
-- Professional monochrome theme
-- Responsive email design
-- Clean metadata tables
-
-### Security
-- All output sanitized with `htmlspecialchars()`
-- Safe HTML rendering
-- No XSS vulnerabilities
-
-### User Experience
-- Event-specific emojis for quick scanning
-- Message content highlighted in message boxes
-- Error information clearly displayed in error boxes
-- Contact custom fields supported
-- Clickable URLs for shortlinks
-
-## ๐ Testing
-
-### Manual Testing
-```bash
-# Test submitted event
-curl -X POST https://your-domain.com/test%40example.com/wxinteract \
- -H "Content-Type: application/json" \
- -d @wxinteract_test_samples.json
-```
-
-### Validation Checklist
-- โ
PHP syntax validated (`php -l index.php`)
-- โ
Parser function exists and is callable
-- โ
All 7 event types have dedicated handlers
-- โ
HTML sanitization implemented
-- โ
Graceful fallback to default format on parse failure
-- โ
Documentation complete
-- โ
Test samples provided
-- โ
Follows existing parser conventions
-
-## ๐ Files Modified/Added
-
-### Modified
-1. `index.php` - Added parser and helper functions (~600 lines)
-2. `README.md` - Added Webex Interact section
-
-### Added
-1. `WEBEX_INTERACT_GUIDE.md` - Complete usage guide
-2. `wxinteract_test_samples.json` - Test payloads
-3. `IMPLEMENTATION_SUMMARY.md` - This file
-
-## ๐ Deployment
-
-### No Additional Requirements
-- Uses existing PHP mail() function
-- No new dependencies
-- No database changes
-- Backward compatible
-
-### Immediate Availability
-The parser is immediately available at:
-```
-https://your-domain.com/{your-email}/wxinteract
-```
-
-## ๐ก Usage Examples
-
-### Production Use
-```
-https://webhooks.dixon.cx/sms-alerts%40mycompany.com/wxinteract
-```
-
-### Testing/Development
-```
-https://webhooks.dixon.cx/dev%40mycompany.com/wxinteract
-```
-
-### Campaign-Specific
-```
-https://webhooks.dixon.cx/sms+summer-campaign%40mycompany.com/wxinteract
-```
-
-## ๐ Code Statistics
-
-- **Total lines added:** ~600 lines
-- **Parser function:** 1 main + 7 helpers
-- **Event types supported:** 7
-- **Test samples:** 7 complete payloads
-- **Documentation:** 3 files updated/created
-
-## ๐ Quality Assurance
-
-### Code Quality
-- โ
Follows PSR standards
-- โ
Consistent naming conventions
-- โ
Proper error handling
-- โ
DRY principle (helper functions)
-- โ
Clear comments and documentation
-
-### Security
-- โ
Input validation
-- โ
HTML sanitization
-- โ
Safe error messages
-- โ
No sensitive data leakage
-
-### Maintainability
-- โ
Modular design
-- โ
Helper functions for each event type
-- โ
Easy to extend for new events
-- โ
Comprehensive documentation
-
-## ๐ Knowledge Transfer
-
-### For Developers
-- Review `WEBEX_INTERACT_GUIDE.md` for API details
-- Check `wxinteract_test_samples.json` for payload examples
-- See helper functions for HTML formatting patterns
-
-### For End Users
-- Configure webhook URL: `/{email}/wxinteract`
-- Select desired events in Webex Interact dashboard
-- Check email for formatted notifications
-
-### For Administrators
-- No server configuration changes needed
-- PHP mail() must be enabled (already required)
-- No additional security considerations
-
-## ๐ Future Enhancements
-
-Possible improvements:
-1. Add webhook signature verification (if Webex Interact supports it)
-2. Support for MMS attachments display
-3. Delivery analytics summary emails
-4. Custom templates per email address
-5. Integration with other Webex Interact APIs
-
-## โจ Summary
-
-A complete, production-ready Webex Interact webhook parser has been implemented following best practices:
-
-- **Secure:** All inputs sanitized
-- **Reliable:** Graceful error handling
-- **Maintainable:** Clean, modular code
-- **Documented:** Comprehensive guides
-- **Tested:** Sample payloads provided
-- **Consistent:** Matches existing parser style
-
-The parser is ready for immediate use!
-
----
-
-**Implementation Date:** 2026-01-22
-**Version:** 2.1.0
-**Author:** GitHub Copilot CLI
diff --git a/PARSER_PROMPT.md b/PARSER_PROMPT.md
index d34e281..11b680c 100644
--- a/PARSER_PROMPT.md
+++ b/PARSER_PROMPT.md
@@ -1,10 +1,10 @@
# Webhook Parser Development Guide
-This guide explains how to create custom webhook parsers for the Webhook Email Forwarder system.
+This guide explains how to create custom webhook parsers for the Webhook Email Forwarder.
## ๐ Overview
-Parsers allow you to format webhook data from specific services (like GitHub, Stripe, GitLab, etc.) into beautifully formatted HTML emails, rather than receiving raw webhook data.
+Parsers format webhook data from specific services (GitHub, Grafana, Stripe, GitLab, etc.) into readable HTML emails instead of the raw request dump.
## ๐ Quick Start
@@ -23,14 +23,13 @@ https://your-domain.com/{email}/{parser-name}
### How It Works
-1. User configures webhook URL with their email and parser name
-2. Service sends webhook to your URL
-3. System looks for `parsers/{parser-name}.php` and a `{parser-name}Parse()` function inside it
-4. Parser function processes the webhook data
-5. Formatted email is sent to the user
-6. The caller gets a JSON acknowledgement that includes the parser name and the email subject
+1. User configures the webhook URL with their email and parser name
+2. Service sends a webhook to that URL
+3. The server looks the parser name up in the `parsers` map in `main.go`
+4. The parser turns the request into an email body and subject
+5. The email is sent and the caller gets a JSON acknowledgement naming the parser and subject
-If no parser is specified, the file doesn't exist, or the parser returns `false`, the system falls back to the default raw webhook format.
+If no parser is named, the name is unknown, or the parser returns `ok == false`, the default raw-request email is used instead.
---
@@ -38,210 +37,173 @@ If no parser is specified, the file doesn't exist, or the parser returns `false`
### File and Function
-Create `parsers/{servicename}.php`. The file is loaded on demand with `require_once`, and `parsers/helpers.php` is already loaded before it, so `emailShell()`, `esc()`, and `renderDataAsTable()` are available.
+Create `parser_{servicename}.go` in the package root with one function of this type:
-Every parser must follow this exact signature:
+```go
+// Parser turns a raw webhook into an email. ok=false means "fall back to the default email".
+type Parser func(raw []byte, headers http.Header, meta Meta) (html, subject string, ok bool)
+```
-```php
- '2026-01-22 15:30:45', // When webhook was received (Y-m-d H:i:s)
- 'ip' => '203.0.113.7', // Client IP (the address nginx forwards, not the proxy)
- 'method' => 'POST', // HTTP method
- 'contentType' => 'application/json' // Content-Type header
-]
+#### `meta Meta`
+```go
+type Meta struct {
+ Date string // "2026-01-22 15:30:45"
+ IP string // the client address nginx forwards, not the proxy
+ Method string // "POST"
+ ContentType string // "application/json"
+}
```
### Return Value
-**Success:** Return an array with two keys:
-
-```php
-return [
- 'html' => $htmlEmailBody, // Complete HTML email (string)
- 'subject' => $emailSubject // Email subject line (string)
-];
-```
-
-**Failure:** Return `false` to fall back to default formatting:
-
-```php
-if (!$payload || !isset($payload['required_field'])) {
- return false; // Will use default webhook email format
-}
-```
+- **Success:** the complete HTML document (from `emailShell`), the subject line, and `true`
+- **Failure:** `"", "", false` to fall back to the default email
---
## ๐จ Building the Email
-You do not write a full HTML document. Build the **body** out of the shared building blocks, then hand it to `emailShell()`, which adds the stylesheet, the gradient header, and the footer. Every parser looks the same this way and design changes happen in one place.
+Never write a full HTML document by hand. Compose the body from the helpers in `render.go` and wrap it with `emailShell`, which adds the stylesheet, gradient header, and footer. Every parser looks the same this way and design changes happen in one place.
-```php
-$html = emailShell($title, $timestamp, $body, $footerSentence);
+```go
+html := emailShell(title, stamp, body, footerSentence)
```
| Argument | What it is |
|----------|------------|
-| `$title` | Header text, already HTML-safe. Start with an emoji. |
-| `$timestamp` | Usually `$metadata['date']`, escaped. Shown as a pill under the title. |
-| `$body` | Your sections (see below). |
-| `$footerSentence` | Optional. One line, e.g. "This Stripe webhook was automatically forwarded to your email address." |
+| `title` | Header text, already HTML-safe. Start with an emoji. |
+| `stamp` | Usually `h(meta.Date)`. Shown as a pill under the title. |
+| `body` | Your sections. |
+| `footerSentence` | One line, e.g. "This Stripe webhook was automatically forwarded to your email address." Empty string for the generic default. |
### Building Blocks
-**Section** with a title:
-
-```html
-
-
๐ณ Payment Details
- ...
-
+```go
+section(title, innerHTML string) string // with a title
+table(rows ...string) string // a real