Files
TK-Wiki-Newsletter-azillner/index.php

1376 lines
53 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
/**
* Wiki Newsletter - Thomas-Krenn Wiki
*
* Voraussetzungen:
* composer require phpmailer/phpmailer
*
* Aufruf im Browser:
* https://deine-domain.tld/wiki-newsletter.php
*
* Cronjob monatlich:
* 0 8 1 * * /usr/bin/php /pfad/wiki-newsletter.php --cron --token='DEIN_CRON_TOKEN'
*/
declare(strict_types=1);
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as MailException;
session_start();
require_once __DIR__ . '/vendor/autoload.php';
const ADMIN_PASSWORD = 'CHANGE_ME_ADMIN_PASSWORD'; // Unbedingt ändern!
const CONFIG_FILE = __DIR__ . '/newsletter-config.json';
const STATE_FILE = __DIR__ . '/newsletter-state.json';
$defaultConfig = [
'wiki_url' => 'https://www.thomas-krenn.com/de/wiki/Neueste_Artikel',
'article_limit' => 20,
'article_fetch_limit' => 150,
'period_mode' => 'previous_month', // previous_month oder current_month
'smtp_host' => '',
'smtp_port' => 587,
'smtp_encryption' => 'tls', // tls, ssl, none
'smtp_user' => '',
'smtp_pass' => '',
'from_email' => '',
'from_name' => 'Wiki-Team',
'recipient_email' => '',
'test_recipient_email' => '',
'subject' => 'Wiki Newsletter - {MONAT}',
'firstname' => 'zusammen',
'editor_tip' => '',
'highlight_url_1' => '',
'highlight_url_2' => '',
'highlight_url_3' => '',
'reply_to' => '',
'unsubscribe_url' => '#',
'cron_token' => bin2hex(random_bytes(24)),
];
function h(?string $value): string
{
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function loadJsonFile(string $file, array $default): array
{
if (!is_file($file)) {
return $default;
}
$json = file_get_contents($file);
$data = json_decode((string)$json, true);
if (!is_array($data)) {
return $default;
}
return array_replace($default, $data);
}
function saveJsonFile(string $file, array $data): void
{
file_put_contents(
$file,
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
LOCK_EX
);
}
function isAdminLoggedIn(): bool
{
if (ADMIN_PASSWORD === 'CHANGE_ME_ADMIN_PASSWORD') {
return true; // Ersteinrichtung. Danach ADMIN_PASSWORD ändern.
}
return !empty($_SESSION['wiki_newsletter_admin']);
}
function requireAdmin(): void
{
if (!isAdminLoggedIn()) {
http_response_code(403);
exit('Nicht angemeldet.');
}
}
function cliArg(string $name): ?string
{
global $argv;
foreach (($argv ?? []) as $arg) {
if ($arg === '--' . $name) {
return '1';
}
if (str_starts_with($arg, '--' . $name . '=')) {
return substr($arg, strlen('--' . $name . '='));
}
}
return null;
}
function fetchUrl(string $url): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 25,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_USERAGENT => 'Wiki-Newsletter/1.0 (+https://www.thomas-krenn.com/de/wiki/Hauptseite)',
CURLOPT_HTTPHEADER => ['Accept: text/html,application/xhtml+xml'],
]);
$html = curl_exec($ch);
$error = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($html === false || $code >= 400) {
throw new RuntimeException('Wiki konnte nicht geladen werden. HTTP-Code: ' . $code . ' Fehler: ' . $error);
}
return (string)$html;
}
function absoluteWikiUrl(string $href): string
{
if (preg_match('~^https?://~i', $href)) {
return $href;
}
if (str_starts_with($href, '//')) {
return 'https:' . $href;
}
if (str_starts_with($href, '/')) {
return 'https://www.thomas-krenn.com' . $href;
}
return 'https://www.thomas-krenn.com/de/wiki/' . ltrim($href, '/');
}
function fetchLatestArticles(string $wikiUrl, int $limit = 150): array
{
$html = fetchUrl($wikiUrl);
$articles = [];
if (class_exists('DOMDocument') && class_exists('DOMXPath')) {
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$nodes = $xpath->query('//li');
foreach ($nodes as $li) {
if (!$li instanceof DOMElement) {
continue;
}
$text = trim(preg_replace('/\s+/', ' ', $li->textContent ?? ''));
if (!preg_match('/\((\d{2}\.\d{2}\.\d{4})\)/', $text, $dateMatch)) {
continue;
}
$links = $li->getElementsByTagName('a');
if ($links->length < 1) {
continue;
}
$title = trim($links->item(0)->textContent ?? '');
$href = trim($links->item(0)->getAttribute('href'));
if ($title === '' || $href === '') {
continue;
}
$author = 'Unbekannt';
if ($links->length >= 2) {
$author = trim($links->item(1)->textContent ?? 'Unbekannt');
}
$date = DateTimeImmutable::createFromFormat('d.m.Y', $dateMatch[1]);
if (!$date) {
continue;
}
$articles[] = [
'title' => $title,
'url' => absoluteWikiUrl($href),
'date' => $date->format('Y-m-d'),
'date_de' => $date->format('d.m.Y'),
'author' => $author,
'sourceText' => $text,
];
if (count($articles) >= $limit) {
break;
}
}
} else {
// Fallback ohne ext-dom: robuster Regex-Parser für die Wiki-Liste
if (preg_match_all('/<li\b[^>]*>(.*?)<\/li>/is', $html, $liMatches)) {
foreach ($liMatches[1] as $liHtml) {
$text = trim(preg_replace('/\s+/', ' ', strip_tags($liHtml)));
if (!preg_match('/\((\d{2}\.\d{2}\.\d{4})\)/', $text, $dateMatch)) {
continue;
}
preg_match_all('/<a\b[^>]*href=(["\'])(.*?)\1[^>]*>(.*?)<\/a>/is', $liHtml, $linkMatches, PREG_SET_ORDER);
if (count($linkMatches) < 1) {
continue;
}
$title = trim(html_entity_decode(strip_tags($linkMatches[0][3]), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
$href = trim(html_entity_decode($linkMatches[0][2], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
if ($title === '' || $href === '') {
continue;
}
$author = 'Unbekannt';
if (count($linkMatches) >= 2) {
$author = trim(html_entity_decode(strip_tags($linkMatches[1][3]), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
if ($author === '') {
$author = 'Unbekannt';
}
}
$date = DateTimeImmutable::createFromFormat('d.m.Y', $dateMatch[1]);
if (!$date) {
continue;
}
$articles[] = [
'title' => $title,
'url' => absoluteWikiUrl($href),
'date' => $date->format('Y-m-d'),
'date_de' => $date->format('d.m.Y'),
'author' => $author,
'sourceText' => $text,
];
if (count($articles) >= $limit) {
break;
}
}
}
}
return $articles;
}
function germanMonthName(int $month): string
{
$months = [
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember'
];
return $months[$month] ?? (string)$month;
}
function newsletterPeriod(string $mode): array
{
$now = new DateTimeImmutable('now');
if ($mode === 'current_month') {
$start = $now->modify('first day of this month')->setTime(0, 0, 0);
$end = $now->modify('tomorrow')->setTime(0, 0, 0);
} else {
$start = $now->modify('first day of previous month')->setTime(0, 0, 0);
$end = $now->modify('first day of this month')->setTime(0, 0, 0);
}
$label = germanMonthName((int)$start->format('n')) . ' ' . $start->format('Y');
$key = $start->format('Y-m');
return [$start, $end, $label, $key];
}
function filterArticlesForPeriod(array $articles, DateTimeImmutable $start, DateTimeImmutable $end, int $limit): array
{
$filtered = [];
foreach ($articles as $article) {
$date = new DateTimeImmutable($article['date'] . ' 00:00:00');
if ($date >= $start && $date < $end) {
$filtered[] = $article;
}
if (count($filtered) >= $limit) {
break;
}
}
return $filtered;
}
function subjectFromTemplate(string $subject, string $periodLabel, int $count): string
{
return str_replace(
['{MONAT}', '{ANZAHL}'],
[$periodLabel, (string)$count],
$subject
);
}
function newsletterHtmlFragmentForClipboard(string $html): string
{
if (preg_match('/<body[^>]*>(.*)<\/body>/is', $html, $matches)) {
return trim($matches[1]);
}
return trim($html);
}
function buildNewsletterHtml(array $config, array $articles, string $periodLabel, bool $isTest = false): string
{
$count = count($articles);
$preheader = $count === 1
? '1 neuer Wiki-Artikel im ' . $periodLabel
: $count . ' neue Wiki-Artikel im ' . $periodLabel;
$replyEmail = trim((string)($config['reply_to'] ?: $config['from_email']));
$replyHref = 'mailto:' . $replyEmail;
$summaryText = $count === 1
? 'In diesem Zeitraum wurde ein neuer Wiki-Artikel veröffentlicht.'
: 'In diesem Zeitraum wurden ' . $count . ' neue Wiki-Artikel veröffentlicht.';
$heroHeadline = 'Die wichtigsten Wiki-Updates fuer Ihr Team';
$editorTip = trim((string)($config['editor_tip'] ?? ''));
$manualHighlightUrls = [];
foreach (['highlight_url_1', 'highlight_url_2', 'highlight_url_3'] as $highlightField) {
$url = trim((string)($config[$highlightField] ?? ''));
if ($url !== '') {
$manualHighlightUrls[] = $url;
}
}
$highlightArticles = [];
if (!empty($manualHighlightUrls)) {
foreach ($manualHighlightUrls as $manualUrl) {
foreach ($articles as $article) {
if (($article['url'] ?? '') === $manualUrl) {
$highlightArticles[] = $article;
break;
}
}
}
}
if (empty($highlightArticles) && !empty($articles)) {
$highlightArticles = array_slice($articles, 0, 3);
}
[$periodStart, $periodEnd] = newsletterPeriod((string)$config['period_mode']);
$rangeEnd = $periodEnd;
if (($config['period_mode'] ?? '') === 'current_month') {
$rangeEnd = new DateTimeImmutable('today');
}
$dateRangeText = 'vom ' . $periodStart->format('d.m') . ' bis ' . $rangeEnd->format('d.m');
$highlightsRows = '';
if (!empty($highlightArticles)) {
$maxHighlights = min(3, count($highlightArticles));
for ($i = 0; $i < $maxHighlights; $i++) {
$highlightsRows .= '
<tr>
<td style="padding:0 0 8px 0;font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:22px;color:#2f3237;">
<span style="display:inline-block;min-width:22px;font-weight:700;color:#ff7d00;">' . ($i + 1) . '.</span>
<a href="' . h($highlightArticles[$i]['url']) . '" target="_blank" style="color:#2f3237;text-decoration:none;">' . h($highlightArticles[$i]['title']) . '</a>
</td>
</tr>';
}
}
$highlightsSection = '';
if ($highlightsRows !== '') {
$highlightsSection = '
<tr>
<td style="padding:0 32px 20px 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%;background:#fff7f0;border:1px solid #ffd6b0;border-radius:12px;">
<tr>
<td style="padding:16px 18px 8px 18px;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:15px;color:#8e4e12;font-weight:700;letter-spacing:.8px;text-transform:uppercase;">
Top Highlights
</td>
</tr>
<tr>
<td style="padding:0 18px 12px 18px;">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0">' . $highlightsRows . '</table>
</td>
</tr>
</table>
</td>
</tr>';
}
$articleRows = '';
if ($count === 0) {
$articleRows = '
<tr>
<td style="padding:0 32px 24px 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%;border:1px solid #d8e0ea;background:#ffffff;border-radius:14px;">
<tr>
<td style="padding:22px;font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:23px;color:#2e3b4d;">
Für diesen Zeitraum wurden keine neuen Artikel gefunden.
</td>
</tr>
</table>
</td>
</tr>';
} else {
foreach ($articles as $article) {
$articleRows .= '
<tr>
<td style="padding:0 32px 16px 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%;background:#ffffff;border:1px solid #d7dee8;border-radius:14px;">
<tr>
<td style="padding:18px 22px 4px 22px;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:16px;color:#5f6d82;font-weight:700;letter-spacing:.7px;text-transform:uppercase;">
Wiki-Beitrag
</td>
</tr>
<tr>
<td class="mobile-card-title" style="padding:0 22px 6px 22px;font-family:Arial,Helvetica,sans-serif;font-size:24px;line-height:32px;font-weight:700;color:#2f3237;">
<a href="' . h($article['url']) . '" target="_blank" style="color:#2f3237;text-decoration:none;">' . h($article['title']) . '</a>
</td>
</tr>
<tr>
<td style="padding:0 22px 8px 22px;font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:24px;color:#40444b;">
Kompakte technische Einordnung und direkte Handlungsempfehlungen aus dem Thomas-Krenn-Wiki.
</td>
</tr>
<tr>
<td style="padding:0 22px 16px 22px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;color:#667086;">
Veröffentlicht am ' . h($article['date_de']) . ' von ' . h($article['author']) . '.
</td>
</tr>
<tr>
<td style="padding:0 22px 20px 22px;">
<table role="presentation" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ff7d00" style="background:#ff7d00;">
<a href="' . h($article['url']) . '" target="_blank" style="display:inline-block;padding:11px 18px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:16px;font-weight:700;color:#ffffff;text-decoration:none;">Zum Beitrag</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>';
}
}
$testBadge = $isTest
? '<tr>
<td style="padding:0 32px 14px 32px;" class="mobile-padding">
<table role="presentation" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#fff4e6" style="background:#fff4e6;border:1px solid #ffd2a3;padding:8px 12px;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:14px;font-weight:700;color:#a34f00;">
TESTVERSAND
</td>
</tr>
</table>
</td>
</tr>'
: '';
$unsubscribeFooter = '';
$unsubscribeUrl = trim((string)($config['unsubscribe_url'] ?? ''));
if ($unsubscribeUrl !== '' && $unsubscribeUrl !== '#' && filter_var($unsubscribeUrl, FILTER_VALIDATE_URL)) {
$unsubscribeFooter = '<br><a href="' . h($unsubscribeUrl) . '" target="_blank" style="color:#ff7d00;text-decoration:underline;">Abmelden</a>';
}
$editorTipSection = '';
if ($editorTip !== '') {
$editorTipSection = '
<tr>
<td style="padding:8px 32px 24px 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%;background:#f7f8fa;border:1px solid #e1e5eb;border-radius:12px;">
<tr>
<td style="padding:16px 18px 8px 18px;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:15px;color:#5f6064;font-weight:700;letter-spacing:.8px;text-transform:uppercase;">
Redaktionstipp
</td>
</tr>
<tr>
<td style="padding:0 18px 16px 18px;font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:24px;color:#32363d;">
' . nl2br(h($editorTip)) . '
</td>
</tr>
</table>
</td>
</tr>';
}
return '<!doctype html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Thomas-Krenn Wiki Newsletter</title>
<style type="text/css">
html, body { margin:0 !important; padding:0 !important; width:100% !important; background:#eceef1; }
table, td { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; }
img { border:0; outline:none; text-decoration:none; -ms-interpolation-mode:bicubic; display:block; }
a { text-decoration:none; }
* { -ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; }
.preheader { display:none !important; visibility:hidden; opacity:0; color:transparent; height:0; width:0; max-height:0; max-width:0; overflow:hidden; mso-hide:all; }
@media only screen and (max-width: 680px) {
.email-container { width:100% !important; }
.mobile-padding { padding-left:16px !important; padding-right:16px !important; }
.mobile-intro { font-size:17px !important; line-height:27px !important; }
.mobile-headline { font-size:30px !important; line-height:37px !important; }
.mobile-card-title { font-size:22px !important; line-height:29px !important; }
.mobile-hero-copy { font-size:17px !important; line-height:26px !important; }
}
</style>
</head>
<body style="margin:0;padding:0;background:#eceef1;">
<div class="preheader">' . h($preheader) . '&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;</div>
<center role="article" aria-roledescription="email" lang="de" style="width:100%;background:#eceef1;">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#eceef1" style="width:100%;background:#eceef1;">
<tr>
<td align="center" style="padding:22px 10px;">
<table role="presentation" width="700" border="0" cellspacing="0" cellpadding="0" class="email-container" style="width:100%;max-width:700px;background:#ffffff;border-top:4px solid #ff7d00;">
<tr>
<td align="center" style="padding:24px 32px 18px 32px;background:#5f6064;" class="mobile-padding">
<img src="https://www.thomas-krenn.com/res/pics/tk_logo_340px.png" width="190" alt="Thomas-Krenn Logo" style="display:block;margin:0 auto;border:0;outline:none;text-decoration:none;height:auto;">
</td>
</tr>
<tr>
<td style="padding:0 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%;background:#5f6064;">
<tr>
<td style="padding:30px 26px;font-family:Arial,Helvetica,sans-serif;">
<div style="font-size:12px;line-height:16px;font-weight:700;color:#ff7d00;letter-spacing:1px;text-transform:uppercase;">Thomas-Krenn Wiki Update</div>
<div class="mobile-headline" style="font-size:34px;line-height:40px;font-weight:700;color:#ffffff;margin-top:10px;">' . h($heroHeadline) . '</div>
<div class="mobile-hero-copy" style="font-size:18px;line-height:27px;color:#ffffff;margin-top:10px;">' . h($periodLabel) . ' | ' . h($summaryText) . '</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:22px 32px 22px 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="mobile-intro" style="font-family:Arial,Helvetica,sans-serif;font-size:18px;line-height:29px;color:#22324a;">
Guten Tag,<br><br>
hier ist Ihre kuratierte Uebersicht mit den wichtigsten Beitraegen aus dem Thomas-Krenn-Wiki ' . $dateRangeText . '.<br>
Der Fokus liegt auf praxisnahen Themen, damit Ihr Team schneller zu Entscheidungen kommt.
</td>
</tr>
</table>
</td>
</tr>
' . $highlightsSection . '
' . $testBadge . '
' . $articleRows . '
' . $editorTipSection . '
<tr>
<td style="padding:0 32px 28px 32px;" class="mobile-padding">
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td style="font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:24px;color:#2a3950;">
Haben Sie Fragen oder einen Themenwunsch?
<a href="' . h($replyHref) . '" style="color:#ff7d00;text-decoration:underline;">Schreiben Sie uns gerne direkt.</a><br><br>
Viele Gruesse<br>
Ihr Thomas-Krenn Team
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="background:#5f6064;padding:22px 20px;text-align:center;font-family:Arial,Helvetica,sans-serif;">
<div style="font-size:12px;line-height:19px;color:#eef0f2;">
Thomas-Krenn.AG | Speltenbach-Steinaecker 1 | D-94078 Freyung<br>
Tel.: +49 8551 9150 0 | Fax: +49 8551 9150 55 |
<a href="https://www.thomas-krenn.com/de/wiki/Hauptseite" target="_blank" style="color:#ff7d00;text-decoration:underline;">thomas-krenn.com</a>
' . $unsubscribeFooter . '
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</body>
</html>';
}
function buildPlainText(array $config, array $articles, string $periodLabel, bool $isTest = false): string
{
$lines = [];
if ($isTest) {
$lines[] = '[TESTVERSAND]';
$lines[] = '';
}
$lines[] = 'Wiki Newsletter';
$lines[] = '';
$lines[] = 'Servus,';
$lines[] = '';
$lines[] = 'das Wiki-Team hat im ' . $periodLabel . ' ' . count($articles) . ' Artikel für das Forum veröffentlicht!';
$lines[] = '';
if (empty($articles)) {
$lines[] = 'Für diesen Zeitraum wurden keine neuen Artikel gefunden.';
} else {
foreach ($articles as $article) {
$lines[] = '- ' . $article['title'] . ' von ' . $article['author'] . ' | ' . $article['date_de'];
$lines[] = ' ' . $article['url'];
}
}
$lines[] = '';
$lines[] = 'Du hast Vorschläge, Ideen oder möchtest selber einen Artikel veröffentlichen?';
$lines[] = 'Dann schreib uns eine Mail: ' . ($config['reply_to'] ?: $config['from_email']);
return implode("\n", $lines);
}
function validateConfig(array $config): void
{
$required = [
'smtp_host' => 'SMTP-Host',
'smtp_port' => 'SMTP-Port',
'from_email' => 'Absender-Mail',
'recipient_email' => 'Empfänger-Mail',
];
foreach ($required as $key => $label) {
if (trim((string)$config[$key]) === '') {
throw new RuntimeException($label . ' fehlt.');
}
}
foreach (['from_email', 'recipient_email'] as $mailKey) {
if (!filter_var($config[$mailKey], FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException($mailKey . ' ist keine gültige E-Mail-Adresse.');
}
}
if (!empty($config['test_recipient_email']) && !filter_var($config['test_recipient_email'], FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException('Test-Empfänger ist keine gültige E-Mail-Adresse.');
}
if (!empty($config['reply_to']) && !filter_var($config['reply_to'], FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException('Reply-To ist keine gültige E-Mail-Adresse.');
}
}
function sendMail(array $config, string $to, string $subject, string $html, string $plain): void
{
validateConfig($config);
$mail = new PHPMailer(true);
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->WordWrap = 78;
$mail->XMailer = 'Thomas-Krenn Wiki Newsletter';
$mail->isSMTP();
$mail->Host = $config['smtp_host'];
$mail->Port = (int)$config['smtp_port'];
$mail->SMTPAuth = trim((string)$config['smtp_user']) !== '' || trim((string)$config['smtp_pass']) !== '';
if ($mail->SMTPAuth) {
$mail->Username = $config['smtp_user'];
$mail->Password = $config['smtp_pass'];
}
if ($config['smtp_encryption'] === 'tls') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
} elseif ($config['smtp_encryption'] === 'ssl') {
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
} else {
$mail->SMTPSecure = false;
$mail->SMTPAutoTLS = false;
}
$mail->setFrom($config['from_email'], $config['from_name'] ?: 'Wiki-Team');
if (!empty($config['reply_to'])) {
$mail->addReplyTo($config['reply_to']);
}
$unsubscribeUrl = trim((string)($config['unsubscribe_url'] ?? ''));
if ($unsubscribeUrl !== '' && $unsubscribeUrl !== '#' && filter_var($unsubscribeUrl, FILTER_VALIDATE_URL)) {
$mail->addCustomHeader('List-Unsubscribe', '<' . $unsubscribeUrl . '>');
}
$mail->addAddress($to);
$mail->Subject = $subject;
$mail->isHTML(true);
$mail->Body = $html;
$mail->AltBody = $plain;
$mail->send();
}
function newsletterData(array $config, bool $testMode = false): array
{
[$start, $end, $periodLabel, $periodKey] = newsletterPeriod($config['period_mode']);
$allArticles = fetchLatestArticles($config['wiki_url'], (int)$config['article_fetch_limit']);
// Testversand nutzt denselben Zeitraum wie der Liveversand.
$articles = filterArticlesForPeriod($allArticles, $start, $end, (int)$config['article_limit']);
if ($testMode) {
$periodKey = 'test-' . date('Y-m-d-H-i-s');
}
return [$articles, $periodLabel, $periodKey];
}
function sendNewsletter(array $config, bool $testMode = false): string
{
[$articles, $periodLabel] = newsletterData($config, $testMode);
$to = $testMode && !empty($config['test_recipient_email'])
? $config['test_recipient_email']
: $config['recipient_email'];
$subject = subjectFromTemplate($config['subject'], $periodLabel, count($articles));
if ($testMode) {
$subject = '[TEST] ' . $subject;
}
$html = buildNewsletterHtml($config, $articles, $periodLabel, $testMode);
$plain = buildPlainText($config, $articles, $periodLabel, $testMode);
sendMail($config, $to, $subject, $html, $plain);
return 'Newsletter wurde an ' . $to . ' gesendet. Artikel: ' . count($articles);
}
function sendMonthlyIfDue(array $config): string
{
[$articles, $periodLabel, $periodKey] = newsletterData($config, false);
$state = loadJsonFile(STATE_FILE, []);
if (($state['last_sent_period'] ?? '') === $periodKey) {
return 'Monatsnewsletter für ' . $periodLabel . ' wurde bereits gesendet. Kein erneuter Versand.';
}
$subject = subjectFromTemplate($config['subject'], $periodLabel, count($articles));
$html = buildNewsletterHtml($config, $articles, $periodLabel, false);
$plain = buildPlainText($config, $articles, $periodLabel, false);
sendMail($config, $config['recipient_email'], $subject, $html, $plain);
$state['last_sent_period'] = $periodKey;
$state['last_sent_at'] = date('c');
$state['last_article_count'] = count($articles);
saveJsonFile(STATE_FILE, $state);
return 'Monatsnewsletter für ' . $periodLabel . ' wurde gesendet. Artikel: ' . count($articles);
}
function configFromPost(array $current): array
{
$fields = [
'wiki_url', 'article_limit', 'article_fetch_limit', 'period_mode',
'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_user',
'from_email', 'from_name', 'recipient_email', 'test_recipient_email',
'subject', 'editor_tip', 'highlight_url_1', 'highlight_url_2', 'highlight_url_3',
'reply_to', 'unsubscribe_url', 'cron_token'
];
foreach ($fields as $field) {
if (isset($_POST[$field])) {
$current[$field] = trim((string)$_POST[$field]);
}
}
if (isset($_POST['smtp_pass']) && trim((string)$_POST['smtp_pass']) !== '') {
$current['smtp_pass'] = (string)$_POST['smtp_pass'];
}
$current['article_limit'] = max(1, min(100, (int)$current['article_limit']));
$current['article_fetch_limit'] = max($current['article_limit'], min(500, (int)$current['article_fetch_limit']));
$current['smtp_port'] = (int)$current['smtp_port'];
if (!in_array($current['smtp_encryption'], ['tls', 'ssl', 'none'], true)) {
$current['smtp_encryption'] = 'tls';
}
if (!in_array($current['period_mode'], ['previous_month', 'current_month'], true)) {
$current['period_mode'] = 'previous_month';
}
if (trim($current['cron_token']) === '') {
$current['cron_token'] = bin2hex(random_bytes(24));
}
return $current;
}
$config = loadJsonFile(CONFIG_FILE, $defaultConfig);
$message = '';
$error = '';
$previewHtml = '';
$previewClipboardHtml = '';
$flash = $_SESSION['wiki_newsletter_flash'] ?? null;
if (is_array($flash)) {
$message = (string)($flash['message'] ?? '');
$error = (string)($flash['error'] ?? '');
$previewHtml = (string)($flash['preview_html'] ?? '');
$previewClipboardHtml = (string)($flash['preview_clipboard_html'] ?? '');
unset($_SESSION['wiki_newsletter_flash']);
}
$isCron = (PHP_SAPI === 'cli' && cliArg('cron') !== null) || isset($_GET['cron']);
if ($isCron) {
$givenToken = PHP_SAPI === 'cli' ? (cliArg('token') ?? '') : (string)($_GET['cron'] ?? '');
if (!hash_equals((string)$config['cron_token'], (string)$givenToken)) {
http_response_code(403);
exit("Ungültiger Cron-Token.\n");
}
try {
echo sendMonthlyIfDue($config) . "\n";
} catch (Throwable $e) {
http_response_code(500);
echo 'Fehler: ' . $e->getMessage() . "\n";
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') {
if (ADMIN_PASSWORD !== 'CHANGE_ME_ADMIN_PASSWORD' && hash_equals(ADMIN_PASSWORD, (string)($_POST['admin_password'] ?? ''))) {
$_SESSION['wiki_newsletter_admin'] = true;
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
exit;
}
$error = 'Falsches Admin-Passwort.';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') !== 'login') {
$redirectUrl = strtok($_SERVER['REQUEST_URI'], '?');
try {
requireAdmin();
$config = configFromPost($config);
saveJsonFile(CONFIG_FILE, $config);
$action = $_POST['action'] ?? 'save';
if ($action === 'save') {
$message = 'Einstellungen gespeichert.';
} elseif ($action === 'preview') {
[$articles, $periodLabel] = newsletterData($config, true);
$previewHtml = buildNewsletterHtml($config, $articles, $periodLabel, true);
$previewClipboardHtml = newsletterHtmlFragmentForClipboard($previewHtml);
$message = 'Vorschau wurde erstellt. Einstellungen wurden gespeichert.';
} elseif ($action === 'test_send') {
$message = sendNewsletter($config, true);
} elseif ($action === 'monthly_send') {
$message = sendMonthlyIfDue($config);
}
$_SESSION['wiki_newsletter_flash'] = [
'message' => $message,
'error' => '',
'preview_html' => $previewHtml,
'preview_clipboard_html' => $previewClipboardHtml,
];
} catch (Throwable $e) {
$_SESSION['wiki_newsletter_flash'] = [
'message' => '',
'error' => $e->getMessage(),
'preview_html' => '',
'preview_clipboard_html' => '',
];
}
header('Location: ' . $redirectUrl);
exit;
}
$isLoggedIn = isAdminLoggedIn();
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wiki Newsletter Admin</title>
<style>
:root {
--bg: #f5f7fa;
--card: #ffffff;
--text: #111827;
--muted: #6b7280;
--border: #e5e7eb;
--primary: #2563eb;
--primary-soft: #eff6ff;
--accent: #f97316;
--danger: #dc2626;
--ok: #16a34a;
--radius: 18px;
--shadow: 0 24px 60px rgba(15, 23, 42, .08);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 40px 16px;
background: var(--bg);
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
color: var(--text);
}
.wrap { max-width: 1040px; margin: 0 auto; }
.hero {
padding: 34px 38px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(37,99,235,.08), rgba(249,115,22,.08)), #fff;
border: 1px solid var(--border);
box-shadow: var(--shadow);
margin-bottom: 22px;
}
.hero h1 { margin: 0; font-size: 38px; letter-spacing: -.04em; }
.hero p { margin: 10px 0 0; color: var(--muted); font-size: 16px; line-height: 1.6; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 14px 36px rgba(15,23,42,.05);
padding: 24px;
}
.card.full { grid-column: 1 / -1; }
h2 { margin: 0 0 18px; font-size: 21px; letter-spacing: -.02em; }
label { display: block; margin: 14px 0 7px; color: #374151; font-size: 14px; font-weight: 700; }
input, select, textarea {
width: 100%;
height: 42px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: #fff;
color: var(--text);
font-size: 14px;
outline: none;
}
textarea {
min-height: 110px;
height: auto;
padding: 10px 12px;
resize: vertical;
line-height: 1.5;
}
input:focus, select:focus, textarea:focus { border-color: rgba(37,99,235,.55); box-shadow: 0 0 0 4px rgba(37,99,235,.08); }
.hint { margin: 7px 0 0; color: var(--muted); font-size: 13px; line-height: 1.5; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
button {
height: 42px;
padding: 0 16px;
border: 0;
border-radius: 12px;
background: var(--primary);
color: #fff;
font-weight: 800;
cursor: pointer;
}
button.secondary { background: #111827; }
button.light { background: var(--primary-soft); color: var(--primary); }
button.warn { background: var(--accent); }
.notice, .error, .warning {
margin-bottom: 18px;
padding: 14px 16px;
border-radius: 14px;
font-weight: 700;
line-height: 1.5;
}
.notice { background: #ecfdf5; color: #166534; border: 1px solid #bbf7d0; }
.error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
.warning { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
code { padding: 2px 6px; border-radius: 7px; background: #f3f4f6; }
.preview { width: 100%; min-height: 760px; border: 1px solid var(--border); border-radius: 16px; background: #fff; }
.preview-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.preview-toolbar p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.5; max-width: 520px; }
.preview-source {
display: none;
}
.outlook-copy-buffer {
position: fixed;
left: -10000px;
top: 0;
width: 720px;
opacity: 0;
pointer-events: none;
}
.copy-status {
margin-top: 10px;
color: var(--ok);
font-size: 13px;
font-weight: 700;
min-height: 20px;
}
@media (max-width: 820px) { .grid { grid-template-columns: 1fr; } .hero { padding: 28px 24px; } }
</style>
</head>
<body>
<div class="wrap">
<section class="hero">
<h1>Wiki Newsletter Admin</h1>
<p>Holt die neuesten Artikel aus dem Thomas-Krenn-Wiki, erstellt daraus einen modernen Newsletter und versendet ihn über SMTP.</p>
</section>
<?php if (ADMIN_PASSWORD === 'CHANGE_ME_ADMIN_PASSWORD'): ?>
<div class="warning">Wichtig: Bitte oben im PHP-Code <code>ADMIN_PASSWORD</code> ändern, bevor die Seite öffentlich erreichbar ist.</div>
<?php endif; ?>
<?php if ($message !== ''): ?><div class="notice"><?= h($message) ?></div><?php endif; ?>
<?php if ($error !== ''): ?><div class="error"><?= h($error) ?></div><?php endif; ?>
<?php if (!$isLoggedIn): ?>
<form method="post" class="card">
<input type="hidden" name="action" value="login">
<h2>Anmeldung</h2>
<label>Admin-Passwort</label>
<input type="password" name="admin_password" autocomplete="current-password" required>
<div class="actions"><button type="submit">Einloggen</button></div>
</form>
<?php else: ?>
<form method="post">
<div class="grid">
<section class="card">
<h2>Wiki & Newsletter</h2>
<label>Wiki-Quelle</label>
<input name="wiki_url" value="<?= h($config['wiki_url']) ?>">
<p class="hint">Standard: Thomas-Krenn „Neueste Artikel“.</p>
<label>Zeitraum</label>
<select name="period_mode">
<option value="previous_month" <?= $config['period_mode'] === 'previous_month' ? 'selected' : '' ?>>Vorheriger Monat</option>
<option value="current_month" <?= $config['period_mode'] === 'current_month' ? 'selected' : '' ?>>Aktueller Monat</option>
</select>
<label>Max. Artikel im Newsletter</label>
<input type="number" name="article_limit" min="1" max="100" value="<?= h((string)$config['article_limit']) ?>">
<label>Max. Artikel von Wiki abrufen</label>
<input type="number" name="article_fetch_limit" min="1" max="500" value="<?= h((string)$config['article_fetch_limit']) ?>">
<label>Betreff</label>
<input name="subject" value="<?= h($config['subject']) ?>">
<p class="hint">Platzhalter: <code>{MONAT}</code>, <code>{ANZAHL}</code></p>
<label>Redaktionstipp (optional)</label>
<textarea name="editor_tip" placeholder="Optionaler Hinweis fuer den Newsletter..."><?= h((string)($config['editor_tip'] ?? '')) ?></textarea>
<p class="hint">Wird nur im Newsletter angezeigt, wenn ein Text hinterlegt ist.</p>
<label>Top Highlight 1 URL (optional)</label>
<input name="highlight_url_1" value="<?= h((string)($config['highlight_url_1'] ?? '')) ?>" placeholder="https://www.thomas-krenn.com/de/wiki/...">
<label>Top Highlight 2 URL (optional)</label>
<input name="highlight_url_2" value="<?= h((string)($config['highlight_url_2'] ?? '')) ?>" placeholder="https://www.thomas-krenn.com/de/wiki/...">
<label>Top Highlight 3 URL (optional)</label>
<input name="highlight_url_3" value="<?= h((string)($config['highlight_url_3'] ?? '')) ?>" placeholder="https://www.thomas-krenn.com/de/wiki/...">
<p class="hint">Wenn gesetzt, werden diese 3 Artikel in genau dieser Reihenfolge als Highlights verwendet. Leer = automatische Highlights (erste 3 Artikel).</p>
</section>
<section class="card">
<h2>SMTP Einstellungen</h2>
<label>SMTP Host</label>
<input name="smtp_host" value="<?= h($config['smtp_host']) ?>" placeholder="smtp.example.com">
<label>SMTP Port</label>
<input type="number" name="smtp_port" value="<?= h((string)$config['smtp_port']) ?>">
<label>Verschlüsselung</label>
<select name="smtp_encryption">
<option value="tls" <?= $config['smtp_encryption'] === 'tls' ? 'selected' : '' ?>>STARTTLS / Port 587</option>
<option value="ssl" <?= $config['smtp_encryption'] === 'ssl' ? 'selected' : '' ?>>SSL/TLS / Port 465</option>
<option value="none" <?= $config['smtp_encryption'] === 'none' ? 'selected' : '' ?>>Keine</option>
</select>
<label>SMTP Benutzer</label>
<input name="smtp_user" value="<?= h($config['smtp_user']) ?>" autocomplete="username">
<label>SMTP Passwort</label>
<input type="password" name="smtp_pass" value="" autocomplete="new-password" placeholder="Leer lassen = gespeichertes Passwort behalten">
<label>Absender-Mail</label>
<input name="from_email" value="<?= h($config['from_email']) ?>" placeholder="wiki@example.com">
<label>Absender-Name</label>
<input name="from_name" value="<?= h($config['from_name']) ?>">
<label>Reply-To / Kontakt-Mail</label>
<input name="reply_to" value="<?= h($config['reply_to']) ?>" placeholder="Optional">
</section>
<section class="card">
<h2>Empfänger</h2>
<label>Newsletter-Empfänger</label>
<input name="recipient_email" value="<?= h($config['recipient_email']) ?>" placeholder="ziel@example.com">
<label>Test-Empfänger</label>
<input name="test_recipient_email" value="<?= h($config['test_recipient_email']) ?>" placeholder="Optional, sonst Newsletter-Empfänger">
<label>Abmeldelink</label>
<input name="unsubscribe_url" value="<?= h($config['unsubscribe_url']) ?>" placeholder="https://example.com/newsletter/abmelden">
<p class="hint">Optional. Wird im Footer und als <code>List-Unsubscribe</code>-Header eingebunden.</p>
</section>
<section class="card">
<h2>Automatisierung</h2>
<label>Cron Token</label>
<input name="cron_token" value="<?= h($config['cron_token']) ?>">
<p class="hint">CLI-Cronjob:</p>
<p class="hint"><code>0 8 1 * * /usr/bin/php <?= h(__FILE__) ?> --cron --token='<?= h($config['cron_token']) ?>'</code></p>
<p class="hint">Web-Cronjob:</p>
<p class="hint"><code>0 8 1 * * /usr/bin/curl -fsS 'https://deine-domain.tld/wiki-newsletter.php?cron=<?= h($config['cron_token']) ?>' &gt;/dev/null</code></p>
</section>
<section class="card full">
<h2>Aktionen</h2>
<div class="actions">
<button type="submit" name="action" value="save">Einstellungen speichern</button>
<button type="submit" name="action" value="preview" class="light">Vorschau erzeugen</button>
<button type="submit" name="action" value="test_send" class="secondary">Testnewsletter senden</button>
<button type="submit" name="action" value="monthly_send" class="warn">Monatsnewsletter jetzt senden</button>
</div>
<p class="hint">Der Testnewsletter nutzt die aktuellsten Artikel ohne Monatsfilter. Der Monatsnewsletter nutzt den eingestellten Zeitraum und wird pro Zeitraum nur einmal versendet.</p>
</section>
</div>
</form>
<?php if ($previewHtml !== ''): ?>
<section class="card full" style="margin-top:18px;">
<h2>Vorschau</h2>
<div class="preview-toolbar">
<p>In Outlook: „Für Outlook kopieren“ klicken und in eine neue E-Mail einfügen (Strg+V). Der komplette Newsletter wird im Outlook-HTML-Format kopiert.</p>
<div class="actions" style="margin-top:0;">
<button type="button" id="copy-outlook-rich" class="light">Für Outlook kopieren</button>
<button type="button" id="copy-outlook-html" class="secondary">HTML kopieren</button>
</div>
</div>
<iframe class="preview" id="preview-frame" srcdoc="<?= h($previewHtml) ?>"></iframe>
<textarea class="preview-source" id="preview-html-source" readonly><?= h($previewHtml) ?></textarea>
<script type="application/json" id="preview-clipboard-fragment"><?= json_encode($previewClipboardHtml, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE) ?></script>
<div class="outlook-copy-buffer" id="outlook-copy-buffer" aria-hidden="true"></div>
<div class="copy-status" id="copy-status" aria-live="polite"></div>
</section>
<script>
(function () {
const htmlSource = document.getElementById('preview-html-source');
const fragmentNode = document.getElementById('preview-clipboard-fragment');
const copyBuffer = document.getElementById('outlook-copy-buffer');
const previewFrame = document.getElementById('preview-frame');
const status = document.getElementById('copy-status');
const richButton = document.getElementById('copy-outlook-rich');
const htmlButton = document.getElementById('copy-outlook-html');
if (!htmlSource || !fragmentNode || !copyBuffer || !status || !richButton || !htmlButton) {
return;
}
let clipboardFragment = '';
try {
clipboardFragment = JSON.parse(fragmentNode.textContent || '""');
} catch (error) {
clipboardFragment = '';
}
function showStatus(message) {
status.textContent = message;
window.setTimeout(function () {
if (status.textContent === message) {
status.textContent = '';
}
}, 4000);
}
function stripHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim();
}
function buildClipboardHtml(fragment) {
return (
'<!doctype html>' +
'<html lang="de" xmlns="http://www.w3.org/1999/xhtml" ' +
'xmlns:v="urn:schemas-microsoft-com:vml" ' +
'xmlns:o="urn:schemas-microsoft-com:office:office">' +
'<head><meta charset="utf-8">' +
'<style type="text/css">' +
'body{margin:0;padding:0;background:#eef2f6;}' +
'table,td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}' +
'a{text-decoration:none;}' +
'</style></head><body>' + fragment + '</body></html>'
);
}
function getFragmentHtml() {
if (previewFrame && previewFrame.contentDocument && previewFrame.contentDocument.body) {
return previewFrame.contentDocument.body.innerHTML;
}
if (clipboardFragment) {
return clipboardFragment;
}
const fullHtml = htmlSource.value || '';
const match = fullHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
return match ? match[1] : fullHtml;
}
function copyViaClipboardEvent(fragment) {
copyBuffer.innerHTML = fragment;
const range = document.createRange();
range.selectNodeContents(copyBuffer);
const selection = window.getSelection();
if (!selection) {
return false;
}
selection.removeAllRanges();
selection.addRange(range);
let copied = false;
function onCopy(event) {
event.preventDefault();
event.clipboardData.setData('text/html', buildClipboardHtml(fragment));
event.clipboardData.setData('text/plain', stripHtml(fragment));
copied = true;
}
document.addEventListener('copy', onCopy);
copied = document.execCommand('copy');
document.removeEventListener('copy', onCopy);
selection.removeAllRanges();
copyBuffer.innerHTML = '';
return copied;
}
function copyFromPreviewFrame() {
if (!previewFrame || !previewFrame.contentWindow || !previewFrame.contentDocument) {
return false;
}
const frameDoc = previewFrame.contentDocument;
const frameWindow = previewFrame.contentWindow;
const frameSelection = frameWindow.getSelection();
if (!frameDoc.body || !frameSelection) {
return false;
}
const range = frameDoc.createRange();
range.selectNodeContents(frameDoc.body);
frameSelection.removeAllRanges();
frameSelection.addRange(range);
frameWindow.focus();
const copied = frameDoc.execCommand('copy');
frameSelection.removeAllRanges();
return copied;
}
async function copyForOutlook() {
const fragment = getFragmentHtml();
if (!fragment) {
showStatus('Keine Vorschau zum Kopieren gefunden.');
return;
}
if (copyFromPreviewFrame()) {
showStatus('Newsletter wurde aus der Vorschau für Outlook kopiert.');
return;
}
if (copyViaClipboardEvent(fragment)) {
showStatus('Newsletter wurde im Outlook-kompatiblen HTML-Format kopiert.');
return;
}
if (window.ClipboardItem && navigator.clipboard && navigator.clipboard.write) {
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([buildClipboardHtml(fragment)], { type: 'text/html' }),
'text/plain': new Blob([stripHtml(fragment)], { type: 'text/plain' })
})
]);
showStatus('Newsletter wurde für Outlook kopiert.');
return;
} catch (error) {
// Fallback unten
}
}
htmlSource.value = buildClipboardHtml(fragment);
htmlSource.style.display = 'block';
htmlSource.focus();
htmlSource.select();
showStatus('Automatisches Kopieren nicht möglich. HTML ist markiert bitte mit Strg+C kopieren.');
}
async function copyText(value, successMessage) {
try {
await navigator.clipboard.writeText(value);
showStatus(successMessage);
} catch (error) {
htmlSource.style.display = 'block';
htmlSource.value = value;
htmlSource.focus();
htmlSource.select();
showStatus('HTML markiert. Bitte mit Strg+C kopieren.');
}
}
richButton.addEventListener('click', copyForOutlook);
htmlButton.addEventListener('click', function () {
copyText(htmlSource.value || buildClipboardHtml(getFragmentHtml()), 'HTML-Quelltext wurde kopiert.');
});
})();
</script>
<?php endif; ?>
<?php endif; ?>
</div>
</body>
</html>