diff --git a/docs.htm b/docs.htm
index a1dc5cd..a5741d8 100644
--- a/docs.htm
+++ b/docs.htm
@@ -354,6 +354,102 @@ Invalid email address provided in URL path.
+
+
+
GitHub Webhook Parser
+
Get beautifully formatted emails specifically designed for GitHub webhooks with enhanced event details.
+
+
How to Use
+
Simply add /github to the end of your webhook URL:
+
+https:///{email-address}/github
+
+
+
URL Format Examples
+
+
+
+
+
+ /john%40example.com
+
+
+
+
+
+
+
+ /john%40example.com/github
+
+
+
+
+
+
Supported GitHub Events
+
The GitHub parser provides enhanced formatting for these event types:
+
+
+
+
+
+
📤 Push Events
+
Displays branch, commit count, and individual commit messages with authors.
+
+
+
+
+
+
+
🔀 Pull Request Events
+
Shows PR number, title, state, branches, and direct link to the PR.
+
+
+
+
+
+
+
📝 Issue Events
+
Displays issue number, title, state, and link to the issue.
+
+
+
+
+
+
+
🚀 Release Events
+
Shows release tag, name, and direct link to the release page.
+
+
+
+
+
+
+ All Events Supported: Even if an event isn't specifically formatted above, the parser will display it in a clean, structured format showing all webhook data.
+
+
+
Setting Up in GitHub
+
To configure a GitHub webhook:
+
+ - Go to your repository settings → Webhooks → Add webhook
+ - Set the Payload URL to:
https:///your%40email.com/github
+ - Set Content type to: application/json
+ - Choose which events to receive (or select "Send me everything")
+ - Click "Add webhook"
+
+
+
+ Subject Line: GitHub webhook emails arrive with event-specific subject lines like "📤 GitHub push - username/repo" or "🔀 GitHub pull_request (opened) - username/repo" for easy filtering.
+
+
+
+ Tip: Use email plus addressing like you+github%40example.com to easily filter GitHub webhooks in your inbox.
+
+
+
Common Use Cases
diff --git a/index.php b/index.php
index c2f9f76..e1645d3 100755
--- a/index.php
+++ b/index.php
@@ -90,10 +90,10 @@ $rawBodySafe = htmlspecialchars($rawBody, ENT_QUOTES, 'UTF-8');
// Compile all PHP superglobal variables into an array for debugging purposes
// This provides visibility of all data received with the request
$variables = [
- '_GET' => $_GET,
- '_POST' => $_POST,
- '_REQUEST' => $_REQUEST,
- '_FILES' => $_FILES,
+ 'GET Variables' => $_GET,
+ 'POST Variables' => $_POST,
+ 'Raw Request Body' => $_REQUEST,
+ 'Uploaded Files' => $_FILES,
];
// Convert the variables array to a human-readable string format
@@ -109,21 +109,31 @@ $ipAddressSafe = htmlspecialchars($ipAddress, ENT_QUOTES, 'UTF-8');
$requestMethodSafe = htmlspecialchars($requestMethod, ENT_QUOTES, 'UTF-8');
$contentTypeSafe = htmlspecialchars($contentType, ENT_QUOTES, 'UTF-8');
-// Check if a parser is specified and exists
+// Load parser helpers
+require_once __DIR__ . '/parsers/helpers.php';
+
+// Check if a parser is specified and load it
$html = '';
$subject = '';
-if ($parserName && function_exists($parserName . 'Parse')) {
- $parserFunction = $parserName . 'Parse';
- $parsed = $parserFunction($rawBody, $headersArray, [
- 'date' => $dateReceived,
- 'ip' => $ipAddress,
- 'method' => $requestMethod,
- 'contentType' => $contentType
- ]);
-
- if ($parsed !== false) {
- $html = $parsed['html'];
- $subject = $parsed['subject'];
+if ($parserName) {
+ $parserFile = __DIR__ . '/parsers/' . $parserName . '.php';
+ if (file_exists($parserFile)) {
+ require_once $parserFile;
+ $parserFunction = $parserName . 'Parse';
+
+ if (function_exists($parserFunction)) {
+ $parsed = $parserFunction($rawBody, $headersArray, [
+ 'date' => $dateReceived,
+ 'ip' => $ipAddress,
+ 'method' => $requestMethod,
+ 'contentType' => $contentType
+ ]);
+
+ if ($parsed !== false) {
+ $html = $parsed['html'];
+ $subject = $parsed['subject'];
+ }
+ }
}
}
@@ -304,1093 +314,6 @@ HTML;
$subject = "‼️ Webhook Request Received - $dateReceived";
}
-/**
- * Helper function to render nested arrays/objects as HTML tables
- *
- * @param array $data The data to render
- * @param int $depth Current recursion depth
- * @return string HTML representation of the data
- */
-function renderDataAsTable($data, $depth = 0) {
- if ($depth > 10) {
- return '
Max depth reached
';
- }
-
- $html = '';
-
- if (!is_array($data)) {
- $safe = htmlspecialchars((string)$data, ENT_QUOTES, 'UTF-8');
- return "
$safe
";
- }
-
- // Check if it's an indexed array of similar items (like commits)
- $isIndexedArray = array_keys($data) === range(0, count($data) - 1);
-
- if ($isIndexedArray && count($data) > 0) {
- // Render as a list of items
- foreach ($data as $index => $item) {
- if (is_array($item)) {
- $html .= '
';
- $html .= '
Item ' . ($index + 1) . '
';
- $html .= '
';
- $html .= '
';
- } else {
- $valueSafe = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8');
- $html .= '
• ' . $valueSafe . '
';
- }
- }
- } else {
- // Render as key-value table
- $html .= '
';
- foreach ($data as $key => $value) {
- $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
-
- if (is_array($value) && !empty($value)) {
- // Close current table and start a subsection
- $html .= '
';
- $html .= '
';
- $html .= '
' . $keySafe . '
';
- $html .= renderDataAsTable($value, $depth + 1);
- $html .= '
';
- $html .= '
';
- }
-
- return $html;
-}
-
-/**
- * GitHub Webhook Parser
- * Parses GitHub webhook events into a formatted email
- *
- * @param string $rawBody The raw request body
- * @param array $headers The request headers
- * @param array $metadata Request metadata (date, ip, method, contentType)
- * @return array|false Array with 'html' and 'subject' keys, or false on failure
- */
-function githubParse($rawBody, $headers, $metadata) {
- $payload = json_decode($rawBody, true);
- if (!$payload) {
- return false;
- }
-
- // Determine event type from headers
- $event = $headers['X-GitHub-Event'] ?? $headers['X-Github-Event'] ?? 'Unknown';
- $delivery = $headers['X-GitHub-Delivery'] ?? $headers['X-Github-Delivery'] ?? 'Unknown';
-
- // Extract common data
- $action = $payload['action'] ?? null;
- $repository = $payload['repository']['full_name'] ?? 'Unknown';
- $sender = $payload['sender']['login'] ?? 'Unknown';
-
- // Sanitize for HTML
- $eventSafe = htmlspecialchars($event, ENT_QUOTES, 'UTF-8');
- $actionSafe = $action ? htmlspecialchars($action, ENT_QUOTES, 'UTF-8') : null;
- $repoSafe = htmlspecialchars($repository, ENT_QUOTES, 'UTF-8');
- $senderSafe = htmlspecialchars($sender, ENT_QUOTES, 'UTF-8');
- $dateSafe = htmlspecialchars($metadata['date'], ENT_QUOTES, 'UTF-8');
- $ipSafe = htmlspecialchars($metadata['ip'], ENT_QUOTES, 'UTF-8');
-
- // Build event-specific content
- $eventContent = '';
- $eventIcon = '🔔';
-
- switch ($event) {
- case 'push':
- $eventIcon = '📤';
- $ref = htmlspecialchars($payload['ref'] ?? 'Unknown', ENT_QUOTES, 'UTF-8');
- $commits = $payload['commits'] ?? [];
- $commitCount = count($commits);
-
- $eventContent = <<
-
Push Details
-
-
-HTML;
-
- if ($commitCount > 0) {
- $eventContent .= 'Commits
';
- foreach (array_slice($commits, 0, 10) as $commit) {
- $message = htmlspecialchars($commit['message'] ?? '', ENT_QUOTES, 'UTF-8');
- $author = htmlspecialchars($commit['author']['name'] ?? 'Unknown', ENT_QUOTES, 'UTF-8');
- $sha = htmlspecialchars(substr($commit['id'] ?? '', 0, 7), ENT_QUOTES, 'UTF-8');
- $eventContent .= "
$sha $message by $author
";
- }
- $eventContent .= '
';
- }
- break;
-
- case 'pull_request':
- $eventIcon = '🔀';
- $pr = $payload['pull_request'] ?? [];
- $prNumber = htmlspecialchars($pr['number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $prTitle = htmlspecialchars($pr['title'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $prUrl = htmlspecialchars($pr['html_url'] ?? '#', ENT_QUOTES, 'UTF-8');
- $prState = htmlspecialchars($pr['state'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $prBase = htmlspecialchars($pr['base']['ref'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $prHead = htmlspecialchars($pr['head']['ref'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- $eventContent = <<
- Pull Request Details
-
-
-HTML;
- break;
-
- case 'issues':
- $eventIcon = '📝';
- $issue = $payload['issue'] ?? [];
- $issueNumber = htmlspecialchars($issue['number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $issueTitle = htmlspecialchars($issue['title'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $issueUrl = htmlspecialchars($issue['html_url'] ?? '#', ENT_QUOTES, 'UTF-8');
- $issueState = htmlspecialchars($issue['state'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- $eventContent = <<
- Issue Details
-
-
-HTML;
- break;
-
- case 'release':
- $eventIcon = '🚀';
- $release = $payload['release'] ?? [];
- $tagName = htmlspecialchars($release['tag_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $releaseName = htmlspecialchars($release['name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $releaseUrl = htmlspecialchars($release['html_url'] ?? '#', ENT_QUOTES, 'UTF-8');
-
- $eventContent = <<
- Release Details
-
-
-HTML;
- break;
-
- default:
- // Generic event display - render as structured tables
- $eventContent = 'Event Details
';
- $eventContent .= renderDataAsTable($payload);
- $eventContent .= '
';
- }
-
- $actionDisplay = $actionSafe ? " ($actionSafe)" : '';
-
- $html = <<
-
-
-
-
- GitHub Webhook - $eventSafe
-
-
-
-
-
-
-
-
📋 Repository Information
-
-
-
- $eventContent
-
-
-
-
-
-HTML;
-
- $subject = "$eventIcon GitHub $eventSafe$actionDisplay - $repoSafe";
-
- return [
- 'html' => $html,
- 'subject' => $subject
- ];
-}
-
-/**
- * Webex Interact Webhook Parser
- * Parses Webex Interact SMS webhook events into a formatted email
- *
- * @param string $rawBody The raw request body
- * @param array $headers The request headers
- * @param array $metadata Request metadata (date, ip, method, contentType)
- * @return array|false Array with 'html' and 'subject' keys, or false on failure
- */
-function wxinteractParse($rawBody, $headers, $metadata) {
- $payload = json_decode($rawBody, true);
- if (!$payload || !isset($payload['data'])) {
- return false;
- }
-
- $data = $payload['data'];
- $status = $data['status'] ?? 'Unknown';
-
- // Sanitize common fields
- $statusSafe = htmlspecialchars($status, ENT_QUOTES, 'UTF-8');
- $dateSafe = htmlspecialchars($metadata['date'], ENT_QUOTES, 'UTF-8');
- $ipSafe = htmlspecialchars($metadata['ip'], ENT_QUOTES, 'UTF-8');
-
- // Determine event type and icon
- $eventIcon = '📱';
- $eventTitle = 'SMS Event';
- $eventContent = '';
-
- switch ($status) {
- case 'submitted':
- $eventIcon = '📤';
- $eventTitle = 'Outbound SMS - Submitted';
- $eventContent = buildOutboundSmsSubmittedContent($data);
- break;
-
- case 'delivered':
- $eventIcon = '✅';
- $eventTitle = 'Outbound SMS - Delivered';
- $eventContent = buildOutboundSmsDeliveredContent($data);
- break;
-
- case 'failed':
- $eventIcon = '❌';
- $eventTitle = 'Outbound SMS - Failed';
- $eventContent = buildOutboundSmsFailedContent($data);
- break;
-
- case 'shortlink_clicked':
- $eventIcon = '🔗';
- $eventTitle = 'Shortlink Clicked';
- $eventContent = buildShortlinkClickedContent($data);
- break;
-
- case 'received':
- $eventIcon = '📥';
- $eventTitle = 'Inbound SMS Received';
- $eventContent = buildInboundSmsContent($data);
- break;
-
- case 'opt_out':
- $eventIcon = '🚫';
- $eventTitle = 'SMS Opt Out Received';
- $eventContent = buildOptOutContent($data);
- break;
-
- case 'contact_created':
- case 'contact_updated':
- $eventIcon = '👤';
- $eventTitle = 'Contacts Callback';
- $eventContent = buildContactsCallbackContent($data);
- break;
-
- default:
- // Unknown event type - display all data
- $eventIcon = '📱';
- $eventTitle = "SMS Event: $statusSafe";
- $eventContent = 'Event Details
';
- $eventContent .= renderDataAsTable($data);
- $eventContent .= '
';
- }
-
- $eventTitleSafe = htmlspecialchars($eventTitle, ENT_QUOTES, 'UTF-8');
-
- $html = <<
-
-
-
-
- Webex Interact - $eventTitleSafe
-
-
-
-
-
-
-
-
📋 Event Information
-
-
-
- $eventContent
-
-
-
-
-
-HTML;
-
- $subject = "$eventIcon Webex Interact: $eventTitle";
-
- return [
- 'html' => $html,
- 'subject' => $subject
- ];
-}
-
-/**
- * Build content for Outbound SMS Submitted event
- */
-function buildOutboundSmsSubmittedContent($data) {
- $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $submittedAt = htmlspecialchars($data['submitted_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- return <<
- 📤 SMS Details
-
-
-
-
-
💬 Message Content
-
$message
-
-HTML;
-}
-
-/**
- * Build content for Outbound SMS Delivered event
- */
-function buildOutboundSmsDeliveredContent($data) {
- $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $deliveredAt = htmlspecialchars($data['delivered_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- return <<
- ✅ Delivery Details
-
-
-HTML;
-}
-
-/**
- * Build content for Outbound SMS Failed event
- */
-function buildOutboundSmsFailedContent($data) {
- $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $failedAt = htmlspecialchars($data['failed_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $errorCode = htmlspecialchars($data['error_code'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $errorMessage = htmlspecialchars($data['error_message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- return <<
- ❌ Failure Details
-
-
-
-
-
⚠️ Error Information
-
- Error Code: $errorCode
- Error Message: $errorMessage
-
-
-HTML;
-}
-
-/**
- * Build content for Shortlink Clicked event
- */
-function buildShortlinkClickedContent($data) {
- $shortlink = htmlspecialchars($data['shortlink'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $originalUrl = htmlspecialchars($data['original_url'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $clickedAt = htmlspecialchars($data['clicked_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $userAgent = htmlspecialchars($data['user_agent'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $ipAddress = htmlspecialchars($data['ip_address'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- return <<
- 🔗 Click Details
-
-
-
-
-
🌐 Browser Information
-
$userAgent
-
-HTML;
-}
-
-/**
- * Build content for Inbound SMS Received event
- */
-function buildInboundSmsContent($data) {
- $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $receivedAt = htmlspecialchars($data['received_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- return <<
- 📥 Inbound SMS Details
-
-
-
-
-
💬 Message Content
-
$message
-
-HTML;
-}
-
-/**
- * Build content for SMS Opt Out event
- */
-function buildOptOutContent($data) {
- $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $optOutAt = htmlspecialchars($data['opt_out_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- return <<
- 🚫 Opt Out Details
-
-
-HTML;
-}
-
-/**
- * Build content for Contacts Callback event
- */
-function buildContactsCallbackContent($data) {
- $contactId = htmlspecialchars($data['contact_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $firstName = htmlspecialchars($data['first_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $lastName = htmlspecialchars($data['last_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $email = htmlspecialchars($data['email'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $status = htmlspecialchars($data['status'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $createdAt = htmlspecialchars($data['created_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
- $updatedAt = htmlspecialchars($data['updated_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
-
- $customFieldsHtml = '';
- if (isset($data['custom_fields']) && is_array($data['custom_fields']) && !empty($data['custom_fields'])) {
- $customFieldsHtml = '🏷️ Custom Fields
-HTML;
- }
- $customFieldsHtml .= '
';
- }
-
- return <<
- 👤 Contact Details
-
-
-
- $customFieldsHtml
-HTML;
-}
-
// Build email headers to ensure proper HTML rendering and sender information
// MIME-Version declares email format capabilities
$emailHeaders = "MIME-Version: 1.0\r\n";
diff --git a/parsers/github.php b/parsers/github.php
new file mode 100644
index 0000000..146ccfc
--- /dev/null
+++ b/parsers/github.php
@@ -0,0 +1,396 @@
+
+ Push Details
+
+
+HTML;
+
+ if ($commitCount > 0) {
+ $eventContent .= 'Commits
';
+ foreach (array_slice($commits, 0, 10) as $commit) {
+ $message = htmlspecialchars($commit['message'] ?? '', ENT_QUOTES, 'UTF-8');
+ $author = htmlspecialchars($commit['author']['name'] ?? 'Unknown', ENT_QUOTES, 'UTF-8');
+ $sha = htmlspecialchars(substr($commit['id'] ?? '', 0, 7), ENT_QUOTES, 'UTF-8');
+ $eventContent .= "
$sha $message by $author
";
+ }
+ $eventContent .= '
';
+ }
+ break;
+
+ case 'pull_request':
+ $eventIcon = '🔀';
+ $pr = $payload['pull_request'] ?? [];
+ $prNumber = htmlspecialchars($pr['number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $prTitle = htmlspecialchars($pr['title'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $prUrl = htmlspecialchars($pr['html_url'] ?? '#', ENT_QUOTES, 'UTF-8');
+ $prState = htmlspecialchars($pr['state'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $prBase = htmlspecialchars($pr['base']['ref'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $prHead = htmlspecialchars($pr['head']['ref'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ $eventContent = <<
+ Pull Request Details
+
+
+HTML;
+ break;
+
+ case 'issues':
+ $eventIcon = '📝';
+ $issue = $payload['issue'] ?? [];
+ $issueNumber = htmlspecialchars($issue['number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $issueTitle = htmlspecialchars($issue['title'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $issueUrl = htmlspecialchars($issue['html_url'] ?? '#', ENT_QUOTES, 'UTF-8');
+ $issueState = htmlspecialchars($issue['state'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ $eventContent = <<
+ Issue Details
+
+
+HTML;
+ break;
+
+ case 'release':
+ $eventIcon = '🚀';
+ $release = $payload['release'] ?? [];
+ $tagName = htmlspecialchars($release['tag_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $releaseName = htmlspecialchars($release['name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $releaseUrl = htmlspecialchars($release['html_url'] ?? '#', ENT_QUOTES, 'UTF-8');
+
+ $eventContent = <<
+ Release Details
+
+
+HTML;
+ break;
+
+ default:
+ // Generic event display - render as structured tables
+ $eventContent = 'Event Details
';
+ $eventContent .= renderDataAsTable($payload);
+ $eventContent .= '
';
+ }
+
+ $actionDisplay = $actionSafe ? " ($actionSafe)" : '';
+
+ $html = <<
+
+
+
+
+ GitHub Webhook - $eventSafe
+
+
+
+
+
+
+
+
📋 Repository Information
+
+
+
+ $eventContent
+
+
+
+
+
+HTML;
+
+ $subject = "$eventIcon GitHub $eventSafe$actionDisplay - $repoSafe";
+
+ return [
+ 'html' => $html,
+ 'subject' => $subject
+ ];
+}
diff --git a/parsers/helpers.php b/parsers/helpers.php
new file mode 100644
index 0000000..23c5a06
--- /dev/null
+++ b/parsers/helpers.php
@@ -0,0 +1,91 @@
+ 10) {
+ return 'Max depth reached
';
+ }
+
+ $html = '';
+
+ if (!is_array($data)) {
+ $safe = htmlspecialchars((string)$data, ENT_QUOTES, 'UTF-8');
+ return "$safe
";
+ }
+
+ // Check if it's an indexed array of similar items (like commits)
+ $isIndexedArray = array_keys($data) === range(0, count($data) - 1);
+
+ if ($isIndexedArray && count($data) > 0) {
+ // Render as a list of items
+ foreach ($data as $index => $item) {
+ if (is_array($item)) {
+ $html .= '';
+ $html .= '
Item ' . ($index + 1) . '
';
+ $html .= '
';
+ $html .= '
';
+ } else {
+ $valueSafe = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8');
+ $html .= '• ' . $valueSafe . '
';
+ }
+ }
+ } else {
+ // Render as key-value table
+ $html .= '';
+ foreach ($data as $key => $value) {
+ $keySafe = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
+
+ if (is_array($value) && !empty($value)) {
+ // Close current table and start a subsection
+ $html .= '
';
+ $html .= '';
+ $html .= '
' . $keySafe . '
';
+ $html .= renderDataAsTable($value, $depth + 1);
+ $html .= '
';
+ $html .= '';
+ }
+
+ return $html;
+}
diff --git a/parsers/wxinteract.php b/parsers/wxinteract.php
new file mode 100644
index 0000000..9107ccb
--- /dev/null
+++ b/parsers/wxinteract.php
@@ -0,0 +1,604 @@
+Event Details
';
+ $eventContent .= renderDataAsTable($data);
+ $eventContent .= '';
+ }
+
+ $eventTitleSafe = htmlspecialchars($eventTitle, ENT_QUOTES, 'UTF-8');
+
+ $html = <<
+
+
+
+
+ Webex Interact - $eventTitleSafe
+
+
+
+
+
+
+
+
📋 Event Information
+
+
+
+ $eventContent
+
+
+
+
+
+HTML;
+
+ $subject = "$eventIcon Webex Interact: $eventTitle";
+
+ return [
+ 'html' => $html,
+ 'subject' => $subject
+ ];
+}
+
+/**
+ * Build content for Outbound SMS Submitted event
+ */
+function buildOutboundSmsSubmittedContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $submittedAt = htmlspecialchars($data['submitted_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 📤 SMS Details
+
+
+
+
+
💬 Message Content
+
$message
+
+HTML;
+}
+
+/**
+ * Build content for Outbound SMS Delivered event
+ */
+function buildOutboundSmsDeliveredContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $deliveredAt = htmlspecialchars($data['delivered_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ ✅ Delivery Details
+
+
+HTML;
+}
+
+/**
+ * Build content for Outbound SMS Failed event
+ */
+function buildOutboundSmsFailedContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $failedAt = htmlspecialchars($data['failed_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $errorCode = htmlspecialchars($data['error_code'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $errorMessage = htmlspecialchars($data['error_message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ ❌ Failure Details
+
+
+
+
+
⚠️ Error Information
+
+ Error Code: $errorCode
+ Error Message: $errorMessage
+
+
+HTML;
+}
+
+/**
+ * Build content for Shortlink Clicked event
+ */
+function buildShortlinkClickedContent($data) {
+ $shortlink = htmlspecialchars($data['shortlink'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $originalUrl = htmlspecialchars($data['original_url'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $clickedAt = htmlspecialchars($data['clicked_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $userAgent = htmlspecialchars($data['user_agent'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $ipAddress = htmlspecialchars($data['ip_address'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 🔗 Click Details
+
+
+
+
+
🌐 Browser Information
+
$userAgent
+
+HTML;
+}
+
+/**
+ * Build content for Inbound SMS Received event
+ */
+function buildInboundSmsContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $receivedAt = htmlspecialchars($data['received_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 📥 Inbound SMS Details
+
+
+
+
+
💬 Message Content
+
$message
+
+HTML;
+}
+
+/**
+ * Build content for SMS Opt Out event
+ */
+function buildOptOutContent($data) {
+ $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $optOutAt = htmlspecialchars($data['opt_out_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 🚫 Opt Out Details
+
+
+HTML;
+}
+
+/**
+ * Build content for Contacts Callback event
+ */
+function buildContactsCallbackContent($data) {
+ $contactId = htmlspecialchars($data['contact_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $firstName = htmlspecialchars($data['first_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $lastName = htmlspecialchars($data['last_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $email = htmlspecialchars($data['email'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $status = htmlspecialchars($data['status'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $createdAt = htmlspecialchars($data['created_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $updatedAt = htmlspecialchars($data['updated_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ $customFieldsHtml = '';
+ if (isset($data['custom_fields']) && is_array($data['custom_fields']) && !empty($data['custom_fields'])) {
+ $customFieldsHtml = '🏷️ Custom Fields
+HTML;
+ }
+ $customFieldsHtml .= '
';
+ }
+
+ return <<
+ 👤 Contact Details
+
+
+
+ $customFieldsHtml
+HTML;
+}