OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

TicketMailRobustnessTest.php

5 KB
<?php

declare(strict_types=1);

use OpenNIT\Tickets\MailDeliveryIncomplete;
use OpenNIT\Tickets\TicketManager;

/**
 * Deckt die Bausteine ab, die Mail-Verlust im Ticketmodul verhindern.
 *
 * Abruf: Die Unterscheidung „dieselbe Mail erneut gesehen" (Aufräumen war
 * fehlgeschlagen) gegen „andere Mail mit wiederverwendeter Message-ID"
 * (Scanner, Monitoring) entscheidet, ob eine Mail übersprungen oder als
 * eigenes Ticket aufgenommen wird.
 *
 * Versand: Ein teilweise gescheiterter Versand trägt die bereits versorgten
 * Empfänger, damit der nächste Versuch weder Empfänger auslässt noch doppelt
 * zustellt.
 */
return static function (): void {

    // ── Abruf: Mail-Identität gegen das gespeicherte Ticket ────────────────

    test('Dieselbe Mail wird am Ticket wiedererkannt', function () {
        $ticket = ['from_email' => 'kunde@firma.de', 'subject' => 'Drucker defekt'];

        assertTrue(TicketManager::isSameMailIdentity($ticket, 'kunde@firma.de', 'Drucker defekt'));
        // Groß-/Kleinschreibung der Adresse und Randabstände sind unerheblich
        assertTrue(TicketManager::isSameMailIdentity($ticket, ' Kunde@Firma.DE ', 'Drucker defekt'));
    });

    test('Eine andere Mail mit recycelter Message-ID wird als solche erkannt', function () {
        $ticket = ['from_email' => 'scanner@firma.de', 'subject' => 'Scan 2026-08-20'];

        // Gleicher Absender (das Gerät), aber anderer Betreff → andere Mail
        assertFalse(TicketManager::isSameMailIdentity($ticket, 'scanner@firma.de', 'Scan 2026-08-21'));
        // Anderer Absender → andere Mail, unabhängig vom Betreff
        assertFalse(TicketManager::isSameMailIdentity($ticket, 'monitoring@firma.de', 'Scan 2026-08-20'));
        // Ohne Absender ist keine Zuordnung möglich
        assertFalse(TicketManager::isSameMailIdentity($ticket, '', 'Scan 2026-08-20'));
    });

    test('Der beim Anlegen gekürzte Betreff wird beim Vergleich gleich behandelt', function () {
        $lang     = str_repeat('A', 620);
        $gekuerzt = mb_substr($lang, 0, 500);
        $ticket   = ['from_email' => 'kunde@firma.de', 'subject' => $gekuerzt];

        assertTrue(TicketManager::isSameMailIdentity($ticket, 'kunde@firma.de', $lang));
    });

    test('Eine Mail ohne Betreff trifft die gespeicherten Platzhalter', function () {
        assertTrue(TicketManager::isSameMailIdentity(
            ['from_email' => 'kunde@firma.de', 'subject' => '(kein Betreff)'],
            'kunde@firma.de',
            ''
        ));
        assertTrue(TicketManager::isSameMailIdentity(
            ['from_email' => 'kunde@firma.de', 'subject' => '(E-Mail ohne lesbaren Betreff)'],
            'kunde@firma.de',
            ''
        ));
        // Leerer Betreff gegen einen echten Betreff bleibt eine andere Mail
        assertFalse(TicketManager::isSameMailIdentity(
            ['from_email' => 'kunde@firma.de', 'subject' => 'Drucker defekt'],
            'kunde@firma.de',
            ''
        ));
    });

    // ── Versand: teilweiser Fehlschlag trägt den Zustellfortschritt ────────

    test('MailDeliveryIncomplete transportiert die versorgten Empfänger', function () {
        $e = new MailDeliveryIncomplete('2 Empfänger nicht erreicht', ['kunde@firma.de', 'cc@firma.de']);

        assertTrue($e instanceof \RuntimeException);
        assertSame(['kunde@firma.de', 'cc@firma.de'], $e->sentTo);
        assertTrue(str_contains($e->getMessage(), 'nicht erreicht'));
    });

    // ── SMTP: RFC-konformer DATA-Block ─────────────────────────────────────

    test('Nackte Zeilenumbrüche werden auf CRLF normalisiert', function () {
        assertSame(
            "Zeile 1\r\nZeile 2\r\nZeile 3",
            \OpenNIT\Notifications\Mailer::prepareDataBlock("Zeile 1\nZeile 2\rZeile 3")
        );
        // Bereits korrekte CRLF bleiben unverändert
        assertSame("a\r\nb", \OpenNIT\Notifications\Mailer::prepareDataBlock("a\r\nb"));
    });

    test('Zeilen mit führendem Punkt werden geschützt (Dot-Stuffing)', function () {
        // Eine Zeile, die nur aus einem Punkt besteht, darf die Übertragung
        // nicht beenden — sie wird verdoppelt und vom Server wieder entstufft.
        assertSame(
            "Text\r\n..\r\nMehr Text",
            \OpenNIT\Notifications\Mailer::prepareDataBlock("Text\n.\nMehr Text")
        );
        assertSame(
            "Text\r\n..NET ist toll",
            \OpenNIT\Notifications\Mailer::prepareDataBlock("Text\n.NET ist toll")
        );
        // Auch am Anfang des Blocks
        assertSame("..punkt", \OpenNIT\Notifications\Mailer::prepareDataBlock(".punkt"));
        // Punkte mitten in der Zeile bleiben unangetastet
        assertSame("a.b\r\nc.d", \OpenNIT\Notifications\Mailer::prepareDataBlock("a.b\nc.d"));
    });

    test('Die Ursache eines Abbruchs bleibt als previous erhalten', function () {
        $ursache = new \RuntimeException('SMTP-Verbindung abgerissen');
        $e = new MailDeliveryIncomplete('abgebrochen', ['kunde@firma.de'], $ursache);

        assertSame($ursache, $e->getPrevious());
        assertSame(['kunde@firma.de'], $e->sentTo);
    });
};