/email%40domain.com
*
* @author Dixon
* @version 1.0
*/
// Prevent any output buffering issues
if (ob_get_level()) {
ob_end_clean();
}
// Retrieve the complete request URI from the server superglobal
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
// 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
$path = parse_url($requestUri, PHP_URL_PATH);
// Remove leading and trailing forward slashes from the path
// Example: '/email%40domain.com/' or '/email%40domain.com/github'
$pathParts = explode('/', trim($path, '/'));
// Extract email and optional parser
$emailEncoded = $pathParts[0] ?? '';
$parserName = $pathParts[1] ?? null;
// Decode the URL-encoded email address (e.g., %40 becomes @)
// Example: 'email%40domain.com' becomes 'email@domain.com'
$email = urldecode($emailEncoded);
if(empty($email)) {
include'docs.htm';
die();
}
// Validate the extracted email address using PHP's built-in filter
// This ensures the email is in a valid format before proceeding
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Set HTTP response code to 400 Bad Request
http_response_code(400);
// Return error message to the client
echo 'Invalid email address provided in URL path.';
// Terminate script execution
exit;
}
// Capture the current date and time when the webhook request was received
// Format: YYYY-MM-DD HH:MM:SS (24-hour format)
$dateReceived = date('Y-m-d H:i:s');
// Retrieve the IP address of the client making the request
// Note: This may be a proxy IP if behind a load balancer or CDN
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
// Capture the HTTP method used for this request (GET, POST, PUT, DELETE, etc.)
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'Unknown';
// Retrieve the Content-Type header sent by the client
// Use null coalescing operator to provide a default if not set
$contentType = $_SERVER['CONTENT_TYPE'] ?? 'N/A';
// Retrieve all HTTP headers sent with the request
// getallheaders() returns an associative array of all headers
$headersArray = getallheaders();
// Initialize an empty string to store formatted headers
$headersText = '';
// Iterate through each header and format it as "Key: Value"
foreach ($headersArray as $key => $value) {
// Concatenate each header on a new line
$headersText .= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') . ': ' .
htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . "\n";
}
// Read the raw request body from the input stream
// php://input allows access to the raw POST data regardless of Content-Type
$rawBody = file_get_contents('php://input');
// Sanitise the raw body for HTML output to prevent XSS attacks
$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,
];
// Convert the variables array to a human-readable string format
// The second parameter (true) makes print_r return the output instead of printing it
$variablesText = print_r($variables, true);
// Sanitise the variables text for HTML output
$variablesTextSafe = htmlspecialchars($variablesText, ENT_QUOTES, 'UTF-8');
// Sanitise individual metadata fields for HTML output
$dateReceivedSafe = htmlspecialchars($dateReceived, ENT_QUOTES, 'UTF-8');
$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
$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 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 = <<
Webhook Request Received
📨 Request Headers
$headersText
📦 Request Body
$rawBodySafe
🔧 PHP Variables
$variablesTextSafe
HTML;
// Construct the email subject line with an attention-grabbing emoji and timestamp
$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
HTML;
$subject = "$eventIcon GitHub $eventSafe$actionDisplay - $repoSafe";
return [
'html' => $html,
'subject' => $subject
];
}
// 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";
// From header sets the sender address to a no-reply address at wh.dixon.cx
$emailHeaders .= "From: no-reply@wh.dixon.cx\r\n";
// Send the email using PHP's built-in mail function
// Parameters: recipient, subject, message body, headers
$mailSent = mail($email, $subject, $html, $emailHeaders);
// Check if the email was sent successfully
if (!$mailSent) {
// Log error or handle failure (note: mail() return value is unreliable on some systems)
error_log("Failed to send webhook email to: $email");
}
// Set the Content-Type header for the HTTP response to plain text
header('Content-Type: text/plain; charset=UTF-8');
// Return a simple acknowledgement to the webhook sender
// This confirms the webhook was received and processed
echo "Webhook received and forwarded to: $email\n\n";
// Include a sanitised copy of the REQUEST data in the response for debugging
echo "Request data:\n";
print_r($_REQUEST);