Files
emailtemplate.it/download/emailtemplate_sender.php
2025-12-07 02:18:06 +01:00

309 lines
9.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* EmailTemplate Sender führt Platzhalter-Ersetzungen lokal aus und verschickt die Mail.
*
* Ablauf:
* 1. EmailTemplate ruft diese Datei per HTTPS auf und übergibt Template-ID/-Name,
* Empfänger sowie Platzhalter-Definitionen.
* 2. Dieses Skript ermittelt fehlende Werte (z. B. aus einer lokalen Datenbank),
* ruft anschließend die externe Template-API auf und verschickt die Mail.
*
* Sicherheit:
* - Authentifizierung über einen statischen Token (separat von der Template-API).
* - HTTPS und IP-Allowlists werden empfohlen.
*
* Konfiguration:
* - Direkt im Array $senderConfig anpassen oder eine Datei
* download/emailtemplate.sender.conf.php anlegen, die ein Array zurückgibt.
*/
$senderConfig = [
'token' => getenv('EMAILTEMPLATE_SENDER_TOKEN') ?: 'REPLACE_WITH_SHARED_TOKEN',
'template_api' => [
'base_url' => getenv('EMAILTEMPLATE_API_BASE') ?: 'https://api.emailtemplate.it/external/render',
'token' => getenv('EMAILTEMPLATE_API_TOKEN') ?: 'REPLACE_WITH_TEMPLATE_API_TOKEN',
'timeout' => 15,
],
'db' => [
'dsn' => getenv('EMAILTEMPLATE_LOCAL_DSN') ?: 'mysql:host=127.0.0.1;dbname=example;charset=utf8mb4',
'user' => getenv('EMAILTEMPLATE_LOCAL_DB_USER') ?: 'root',
'pass' => getenv('EMAILTEMPLATE_LOCAL_DB_PASS') ?: '',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
],
'enabled' => true,
],
'mail' => [
'from_email' => 'no-reply@example.com',
'from_name' => 'EmailTemplate Sender',
'transport' => 'mail', // aktuell nur mail()
],
];
$localSenderOverride = __DIR__ . '/emailtemplate.sender.conf.php';
if (is_file($localSenderOverride)) {
$override = include $localSenderOverride;
if (is_array($override)) {
$senderConfig = array_replace_recursive($senderConfig, $override);
}
}
function senderRespond($payload, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function senderRequireToken(array $config): void
{
$expected = (string)($config['token'] ?? '');
if ($expected === '') {
senderRespond(['ok' => false, 'error' => 'Sender token not configured'], 500);
}
$provided = null;
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && stripos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer ') === 0) {
$provided = trim(substr($_SERVER['HTTP_AUTHORIZATION'], 7));
} elseif (!empty($_SERVER['HTTP_X_EMAILTEMPLATE_TOKEN'])) {
$provided = trim($_SERVER['HTTP_X_EMAILTEMPLATE_TOKEN']);
} elseif (isset($_POST['token'])) {
$provided = (string)$_POST['token'];
}
if (!$provided || !hash_equals($expected, $provided)) {
senderRespond(['ok' => false, 'error' => 'Unauthorized'], 403);
}
}
function senderInput(): array
{
$raw = file_get_contents('php://input');
if ($raw !== false && trim($raw) !== '') {
$decoded = json_decode($raw, true);
if (is_array($decoded)) {
return $decoded;
}
}
if (!empty($_POST)) {
return $_POST;
}
return [];
}
function senderDb(array $config): ?PDO
{
if (empty($config['db']['enabled'])) {
return null;
}
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
try {
$pdo = new PDO(
$config['db']['dsn'],
$config['db']['user'],
$config['db']['pass'],
$config['db']['options']
);
} catch (Throwable $e) {
senderRespond(['ok' => false, 'error' => 'DB connection failed', 'detail' => $e->getMessage()], 500);
}
return $pdo;
}
function resolvePlaceholderValue($definition, ?PDO $pdo)
{
if (is_scalar($definition)) {
return $definition;
}
if (!is_array($definition)) {
return null;
}
if (array_key_exists('value', $definition)) {
return $definition['value'];
}
if (($definition['source'] ?? '') === 'db') {
if (!$pdo) {
throw new RuntimeException('Database access disabled');
}
$table = $definition['table'] ?? null;
$column = $definition['column'] ?? null;
$where = $definition['where'] ?? [];
if (!$table || !$column || !is_array($where) || !$where) {
throw new InvalidArgumentException('Invalid DB placeholder definition');
}
$conditions = [];
$params = [];
foreach ($where as $key => $value) {
$param = ':w_' . preg_replace('/[^a-z0-9_]/i', '_', $key);
$conditions[] = sprintf('`%s` = %s', str_replace('`', '', $key), $param);
$params[$param] = $value;
}
$sql = sprintf(
'SELECT `%s` FROM `%s` WHERE %s LIMIT 1',
str_replace('`', '', $column),
str_replace('`', '', $table),
implode(' AND ', $conditions)
);
$stmt = $pdo->prepare($sql);
foreach ($params as $param => $value) {
$stmt->bindValue($param, $value);
}
$stmt->execute();
return $stmt->fetchColumn();
}
return null;
}
function fetchTemplateHtml(array $config, array $payload): array
{
$apiUrl = $config['template_api']['base_url'];
$apiToken = $config['template_api']['token'];
if (!$apiUrl || !$apiToken) {
senderRespond(['ok' => false, 'error' => 'Template API not configured'], 500);
}
$body = $payload;
$body['token'] = $apiToken;
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'timeout' => (int)($config['template_api']['timeout'] ?? 15),
'content' => json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
],
]);
$response = @file_get_contents($apiUrl, false, $context);
if ($response === false) {
senderRespond(['ok' => false, 'error' => 'Template API unreachable'], 502);
}
$decoded = json_decode($response, true);
if (!is_array($decoded) || !empty($decoded['ok']) === false) {
senderRespond(['ok' => false, 'error' => 'Template API error', 'detail' => $decoded], 502);
}
if (empty($decoded['html'])) {
senderRespond(['ok' => false, 'error' => 'Template API returned no HTML'], 502);
}
return $decoded;
}
function senderMail(array $config, string $to, string $subject, string $html): bool
{
$headers = [
'MIME-Version: 1.0',
'Content-Type: text/html; charset=UTF-8',
];
$from = $config['mail']['from_email'] ?? null;
if ($from) {
$fromName = $config['mail']['from_name'] ?? '';
if ($fromName !== '') {
$fromName = function_exists('mb_encode_mimeheader')
? mb_encode_mimeheader($fromName, 'UTF-8')
: $fromName;
$headers[] = sprintf('From: %s <%s>', $fromName, $from);
} else {
$headers[] = 'From: ' . $from;
}
}
$encodedSubject = function_exists('mb_encode_mimeheader')
? mb_encode_mimeheader($subject, 'UTF-8')
: $subject;
return @mail($to, $encodedSubject, $html, implode("\r\n", $headers));
}
senderRequireToken($senderConfig);
$input = senderInput();
$templateId = isset($input['template_id']) ? (int)$input['template_id'] : null;
$templateName = $input['template_name'] ?? null;
$recipient = isset($input['to']) ? filter_var($input['to'], FILTER_VALIDATE_EMAIL) : false;
$subject = trim((string)($input['subject'] ?? 'EmailTemplate Versand'));
$previewOnly = !empty($input['preview_only']);
if (!$templateId && !$templateName) {
senderRespond(['ok' => false, 'error' => 'template_id or template_name required'], 422);
}
if (!$recipient && !$previewOnly) {
senderRespond(['ok' => false, 'error' => 'Valid recipient required'], 422);
}
$placeholdersSpec = $input['placeholders'] ?? [];
if (is_string($placeholdersSpec)) {
$decoded = json_decode($placeholdersSpec, true);
if (is_array($decoded)) {
$placeholdersSpec = $decoded;
}
}
if (!is_array($placeholdersSpec)) {
$placeholdersSpec = [];
}
$pdo = senderDb($senderConfig);
$resolvedPlaceholders = [];
foreach ($placeholdersSpec as $key => $definition) {
try {
$resolvedPlaceholders[$key] = resolvePlaceholderValue($definition, $pdo);
} catch (Throwable $e) {
senderRespond(['ok' => false, 'error' => 'Placeholder resolution failed', 'detail' => $e->getMessage()], 422);
}
}
$templatePayload = [
'placeholders' => $resolvedPlaceholders,
];
if ($templateId) {
$templatePayload['template_id'] = $templateId;
}
if ($templateName) {
$templatePayload['template_name'] = $templateName;
}
$rendered = fetchTemplateHtml($senderConfig, $templatePayload);
$finalHtml = (string)$rendered['html'];
if (isset($rendered['subject']) && !empty($rendered['subject'])) {
$subject = (string)$rendered['subject'];
}
if ($previewOnly) {
senderRespond([
'ok' => true,
'mode' => 'preview',
'subject' => $subject,
'html' => $finalHtml,
'placeholders' => $resolvedPlaceholders,
]);
}
$sent = senderMail($senderConfig, $recipient, $subject, $finalHtml);
if (!$sent) {
senderRespond(['ok' => false, 'error' => 'Mail dispatch failed'], 500);
}
senderRespond([
'ok' => true,
'mode' => 'send',
'recipient' => $recipient,
'subject' => $subject,
'placeholders' => $resolvedPlaceholders,
]);