Upload via GUI (273 Dateien)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
/**
|
||||
* includes/mail_text.php
|
||||
*
|
||||
* Macht aus einer eingehenden E-Mail (IMAP-Struktur) lesbaren Text für das Ticket: Es wird der Text-Teil bevorzugt; gibt es nur HTML
|
||||
* (Newsletter, Benachrichtigungen), wird es so umgewandelt, dass nur sichtbarer Text bleibt. Stil- und Skriptblöcke, versteckte
|
||||
* Vorschautexte, Kommentare und Sonderzeichen für Leerräume (typisch bei Newslettern) fallen weg, Absätze und Listen bleiben als
|
||||
* Zeilen erhalten. Ohne diese Umwandlung stand der CSS-Code der Mail im Ticket.
|
||||
*/
|
||||
|
||||
/** Zeichen, die in Newslettern als unsichtbare Füllung dienen (Breite null, weiche Trennung, Kombinationszeichen ohne Inhalt). */
|
||||
const MAIL_INVISIBLE_CHARS = "\u{200B}\u{200C}\u{200D}\u{200E}\u{200F}\u{2060}\u{FEFF}\u{00AD}\u{034F}\u{061C}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180E}";
|
||||
|
||||
/** Ob das style-Attribut ein Element unsichtbar macht (Vorschautext-Tricks, Abstandshalter). */
|
||||
function mail_style_hides(string $style): bool
|
||||
{
|
||||
return (bool) preg_match('/display\s*:\s*none|visibility\s*:\s*hidden|mso-hide\s*:\s*all|max-height\s*:\s*0(?:px|pt)?\s*(?:;|$)|font-size\s*:\s*0(?:px|pt)?\s*(?:;|$)|opacity\s*:\s*0(?:\.0+)?\s*(?:;|$)/i', $style);
|
||||
}
|
||||
|
||||
/** Wandelt HTML in lesbaren Text um (UTF-8). */
|
||||
function mail_html_to_text(string $html): string
|
||||
{
|
||||
if (trim($html) === '') return '';
|
||||
$text = class_exists('DOMDocument') ? mail_html_to_text_dom($html) : mail_html_to_text_regex($html);
|
||||
return mail_clean_text($text);
|
||||
}
|
||||
|
||||
function mail_html_to_text_dom(string $html): string
|
||||
{
|
||||
// Zeichensatz-Angabe der Mail soll das Einlesen nicht stören: wir übergeben bereits UTF-8
|
||||
$html = preg_replace('/<meta[^>]+charset[^>]*>/i', '', $html) ?? $html;
|
||||
$prev = libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
$ok = $dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NONET);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($prev);
|
||||
if (!$ok || !$dom->documentElement) return mail_html_to_text_regex($html);
|
||||
|
||||
$blocks = ['p', 'div', 'section', 'article', 'header', 'footer', 'table', 'tr', 'ul', 'ol', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'address', 'center', 'form', 'dl', 'dt', 'dd'];
|
||||
$skip = ['script', 'style', 'head', 'title', 'noscript', 'template', 'meta', 'link', 'svg', 'object', 'embed', 'iframe', 'canvas', 'map'];
|
||||
$out = '';
|
||||
// Zeilenwechsel für Blöcke und Listen: nie doppelt hintereinander (ein <br> dagegen erzeugt immer einen)
|
||||
$newline = function () use (&$out): void {
|
||||
if ($out !== '' && !str_ends_with(rtrim($out, ' '), "
|
||||
")) $out = rtrim($out, ' ') . "
|
||||
";
|
||||
};
|
||||
$walk = function (DOMNode $node) use (&$walk, &$out, $blocks, $skip, $newline): void {
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($child instanceof DOMText) {
|
||||
// Umbrüche und Einrückung im Quelltext sind keine Zeilenwechsel
|
||||
$out .= preg_replace('/[ \t\r\n\f]+/', ' ', $child->nodeValue ?? '');
|
||||
} elseif ($child instanceof DOMElement) {
|
||||
$tag = strtolower($child->tagName);
|
||||
if (in_array($tag, $skip, true)) continue;
|
||||
if ($child->hasAttribute('hidden') || mail_style_hides($child->getAttribute('style'))) continue;
|
||||
if ($tag === 'br') { $out .= "\n"; continue; }
|
||||
if ($tag === 'img') { $alt = trim($child->getAttribute('alt')); if ($alt !== '') $out .= ' ' . $alt . ' '; continue; }
|
||||
$block = in_array($tag, $blocks, true);
|
||||
if ($block) $newline();
|
||||
if ($tag === 'li') { $newline(); $out .= '• '; }
|
||||
if ($tag === 'td' || $tag === 'th') $out .= ' ';
|
||||
$walk($child);
|
||||
if ($tag === 'td' || $tag === 'th') $out .= ' ';
|
||||
if ($block || $tag === 'li') $newline();
|
||||
}
|
||||
}
|
||||
};
|
||||
$walk($dom->documentElement);
|
||||
return $out;
|
||||
}
|
||||
|
||||
function mail_html_to_text_regex(string $html): string
|
||||
{
|
||||
$html = preg_replace('~<!--.*?-->~s', '', $html) ?? $html;
|
||||
$html = preg_replace('~<(script|style|head|title|noscript|template)\b[^>]*>.*?</\1\s*>~is', '', $html) ?? $html;
|
||||
$html = preg_replace('~<br\s*/?>~i', "\n", $html) ?? $html;
|
||||
$html = preg_replace('~</(p|div|tr|li|h[1-6]|table|ul|ol|blockquote)\s*>~i', "\n", $html) ?? $html;
|
||||
$html = preg_replace('~<li\b[^>]*>~i', "\n• ", $html) ?? $html;
|
||||
return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
/** Räumt Text auf: unsichtbare Zeichen, geschützte Leerzeichen, überzählige Leerzeilen. */
|
||||
function mail_clean_text(string $text): string
|
||||
{
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
$text = str_replace("\u{00A0}", ' ', $text);
|
||||
$text = preg_replace('/[' . preg_quote(MAIL_INVISIBLE_CHARS, '/') . ']+/u', '', $text) ?? $text;
|
||||
$lines = [];
|
||||
foreach (explode("\n", $text) as $line) {
|
||||
$lines[] = trim(preg_replace('/[ \t]{2,}/', ' ', $line) ?? $line);
|
||||
}
|
||||
$text = implode("\n", $lines);
|
||||
$text = preg_replace("/\n{3,}/", "\n\n", $text) ?? $text;
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/** Dekodiert den Inhalt eines Mail-Teils (Transferkodierung und Zeichensatz) nach UTF-8. */
|
||||
function mail_decode_part(string $raw, int $encoding, string $charset): string
|
||||
{
|
||||
$data = match ($encoding) {
|
||||
3 => (string) base64_decode(preg_replace('/\s+/', '', $raw) ?? $raw, false),
|
||||
4 => quoted_printable_decode($raw),
|
||||
default => $raw,
|
||||
};
|
||||
$charset = strtolower(trim($charset));
|
||||
if ($charset === '' || $charset === 'utf-8' || $charset === 'utf8' || $charset === 'us-ascii') {
|
||||
return mb_check_encoding($data, 'UTF-8') ? $data : (string) mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
try {
|
||||
return (string) mb_convert_encoding($data, 'UTF-8', $charset);
|
||||
} catch (Throwable) {
|
||||
return mb_check_encoding($data, 'UTF-8') ? $data : (string) mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt die Text-Teile (text/plain, text/html) einer IMAP-Struktur, ohne Anhänge.
|
||||
*
|
||||
* @return array<int,array{no:string,subtype:string,encoding:int,charset:string}>
|
||||
*/
|
||||
function mail_collect_text_parts(object $part, string $no = ''): array
|
||||
{
|
||||
$found = [];
|
||||
$isMulti = !empty($part->parts) && is_array($part->parts);
|
||||
if ($isMulti) {
|
||||
foreach ($part->parts as $i => $sub) {
|
||||
$found = array_merge($found, mail_collect_text_parts($sub, $no === '' ? (string) ($i + 1) : $no . '.' . ($i + 1)));
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
// type 0 = text
|
||||
if ((int) ($part->type ?? -1) !== 0) return [];
|
||||
$disposition = strtolower((string) ($part->disposition ?? ''));
|
||||
$charset = '';
|
||||
$filename = false;
|
||||
foreach (array_merge((array) ($part->parameters ?? []), (array) ($part->dparameters ?? [])) as $p) {
|
||||
$attr = strtolower((string) ($p->attribute ?? ''));
|
||||
if ($attr === 'charset') $charset = (string) $p->value;
|
||||
if ($attr === 'name' || $attr === 'filename') $filename = true;
|
||||
}
|
||||
if ($disposition === 'attachment' || $filename) return [];
|
||||
$found[] = ['no' => $no === '' ? '1' : $no, 'subtype' => strtoupper((string) ($part->subtype ?? 'PLAIN')), 'encoding' => (int) ($part->encoding ?? 0), 'charset' => $charset];
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bester Text einer Mail: bevorzugt text/plain; ist der nur eine kurze Floskel ("Bitte HTML-Ansicht verwenden") und die HTML-Fassung
|
||||
* deutlich länger, wird die HTML-Fassung als Text genommen.
|
||||
*
|
||||
* @param callable(string):string $fetchPart liefert den rohen Inhalt eines Teils (imap_fetchbody)
|
||||
*/
|
||||
function mail_best_text(object $structure, callable $fetchPart): string
|
||||
{
|
||||
$plain = '';
|
||||
$html = '';
|
||||
$havePlain = false;
|
||||
$haveHtml = false;
|
||||
foreach (mail_collect_text_parts($structure) as $p) {
|
||||
if ($p['subtype'] === 'PLAIN' && !$havePlain) {
|
||||
$plain = mail_clean_text(mail_decode_part($fetchPart($p['no']), $p['encoding'], $p['charset']));
|
||||
$havePlain = true;
|
||||
} elseif ($p['subtype'] === 'HTML' && !$haveHtml) {
|
||||
$html = mail_html_to_text(mail_decode_part($fetchPart($p['no']), $p['encoding'], $p['charset']));
|
||||
$haveHtml = true;
|
||||
}
|
||||
}
|
||||
if ($plain !== '' && (mb_strlen($plain) >= 40 || $html === '' || mb_strlen($html) <= mb_strlen($plain) * 2)) return $plain;
|
||||
return $html !== '' ? $html : $plain;
|
||||
}
|
||||
Reference in New Issue
Block a user