4 Commits

Author SHA1 Message Date
schalli 573d070041 docs: Stand nach quick-260921-oxm, Fehlerrueckstand abgearbeitet
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 1m8s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m3s
Elf Quick-Vorgaenge am 2026-09-21. Diagnosen 2923 -> 123, Tests
447 -> 1679, keine auf Fehlerstufe. Aus der Fehlerarbeit ist nichts
mehr offen.

Naechster Auftrag laut Nutzer: zwei neue Dashboard-Widgets. Die
Produktfragen zum ersten (Bilderrahmen) sind geklaert und in
stopped_at festgehalten; das zweite hat der Nutzer noch nicht benannt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 18:05:26 +02:00
schalli 6def5396e4 docs(quick-260921-oxm): Akte - beide IMAP-Befunde behoben, Rot-Nachweis festgehalten
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 18:04:32 +02:00
schalli d0266bf86e fix(quick-260921-oxm): STARTTLS wirklich erzwingen, Anhangs-Dateinamen richtig lesen
B-06: requireTLS durch doSTARTTLS ersetzt. requireTLS kennt imapflow 1.4.3
nicht und verwirft es still; ein als STARTTLS eingerichtetes Postfach konnte
deshalb unbemerkt im Klartext verbinden. Gewollte Folge: so ein Postfach
scheitert jetzt, wenn der Server kein STARTTLS anbietet. Bei ssl-tls ergibt
der Ausdruck false, was die Unvertraeglichkeit secure=true + doSTARTTLS=true
gar nicht erst entstehen laesst.

B-05: Dateiname aus Content-Disposition kommt jetzt aus dem Feld, in dem
imapflow ihn ablegt (dispositionParameters), statt aus .parameters einer
Zeichenkette. Der alte Ausdruck war zur Laufzeit immer undefined, wodurch
Anhaenge als application/octet-stream (typisch Outlook) nicht erkannt wurden.

Die Zusicherung am Ende von buildClient() entfaellt ersatzlos; sie bestand
nur wegen der unbekannten Option. noExplicitAny in apps/api/src faellt damit
von 15 auf 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 18:03:21 +02:00
schalli 7691d1fd6d test(quick-260921-oxm): rote Tests fuer die beiden IMAP-Befunde
- B-06: prueft die an ImapFlow uebergebenen Optionen fuer starttls und ssl-tls
- B-05: prueft, dass ein octet-stream-Anhang am Dateinamen aus
  Content-Disposition erkannt wird
- Einhaengen des Testdoppels in einen Helfer gezogen; die Umdeutung des
  Konstruktors steht damit nur noch an einer Stelle statt an zwoelf

Gegen den heutigen Stand rot: 3 von 12 Faellen scheitern
(doSTARTTLS undefined, Feld requireTLS vorhanden, Anhang nicht eingesammelt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 18:00:53 +02:00
4 changed files with 364 additions and 36 deletions
+5 -4
View File
@@ -4,8 +4,8 @@ milestone: v1.2
current_phase: 18 current_phase: 18
current_phase_name: desktop-client-fertigstellen current_phase_name: desktop-client-fertigstellen
status: verified status: verified
stopped_at: "Zehn Quick-Vorgaenge am 2026-09-21. Der gesamte Lint- und Fehlerrueckstand ist abgearbeitet (2923 → 125 Diagnosen, 0 Fehlerstufe). ZWEI BEFUNDE WARTEN AUF ENTSCHEIDUNG DES NUTZERS, beide aus m34 Aufgabe 3, beide wuerden Verhalten aendern: (B-06, Sicherheit) imap.provider.ts setzt requireTLS, das es in imapflow 1.4.3 nicht gibt — die Einstellung STARTTLS erzwingt nichts und faellt bei fehlender Server-Unterstuetzung unverschluesselt zurueck; richtig waere doSTARTTLS: true, Folge: solche Postfaecher scheitern dann statt im Klartext zu verbinden. (B-05) imap.provider.ts:78 liest .parameters von einer Zeichenkette, Outlook-Anhaenge werden ueber Content-Disposition nicht erkannt, betrifft den DKV-Rechnungseinzug. Danach: zwei neue Dashboard-Widgets, das erste ein Bilderrahmen (Upload ODER https-Webadresse, Browser laedt direkt), das zweite noch unbenannt. m34 ist noch nicht gepusht." stopped_at: "Elf Quick-Vorgaenge am 2026-09-21, alle abgeschlossen und verifiziert. Der gesamte Fehler- und Lint-Rueckstand ist abgearbeitet: 2923 → 123 Diagnosen, 0 Fehlerstufe, Tests 447 → 1679. NICHTS OFFEN aus der Fehlerarbeit. NAECHSTER AUFTRAG: zwei neue Dashboard-Widgets. Das erste ist ein Bilderrahmen, die Produktfragen sind bereits geklaert — Bilder werden hochgeladen ODER per https-Webadresse eingebunden (der Browser laedt Fremdbilder direkt, kein Server-Abruf, damit keine SSRF-Flaeche); Einstellungen: Bildausschnitt (ganz sichtbar oder formatfuellend), Wechselintervall, Reihenfolge oder Zufall, Bildunterschrift, Klick zeigt gross; Stil wie die bestehenden Widgets; Bilder gehoeren dem hochladenden Benutzer, Groesse und Anzahl begrenzt. DAS ZWEITE WIDGET HAT DER NUTZER NOCH NICHT BENANNT — danach fragen. Offen beim Nutzer: alpha ziehen, Windows-Client pruefen, Freigabe 1.3.0 auf Zuruf."
last_updated: "2026-09-21T16:10:00.000Z" last_updated: "2026-09-21T16:45:00.000Z"
last_activity: 2026-09-21 last_activity: 2026-09-21
last_activity_desc: Quick 260921-9ie, a1d, bi2, fi3 und gof — Lint-Tor scharf, Benutzerverwaltung meldet abgewiesene Aktionen, Lint-Rueckstand 2923 → 446, erzwungener Passwortwechsel an der API durchgesetzt (war eine tote Sperre), 21 Effekt-Abhaengigkeiten einzeln beurteilt; alle fuenf verifiziert, die letzten drei am laufenden System last_activity_desc: Quick 260921-9ie, a1d, bi2, fi3 und gof — Lint-Tor scharf, Benutzerverwaltung meldet abgewiesene Aktionen, Lint-Rueckstand 2923 → 446, erzwungener Passwortwechsel an der API durchgesetzt (war eine tote Sperre), 21 Effekt-Abhaengigkeiten einzeln beurteilt; alle fuenf verifiziert, die letzten drei am laufenden System
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2 state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden) Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
Plan: 6 of 6 Plan: 6 of 6
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
Last activity: 2026-09-21 - Quick 260921-m34: 288 any im Backend auf 15 gesenkt, jede verbliebene mit Urteil; dabei vier Befunde gemeldet statt still repariert, darunter ein sicherheitsrelevanter: die IMAP-Einstellung STARTTLS erzwingt nichts, weil die gesetzte Option in imapflow gar nicht existiert Last activity: 2026-09-21 - Quick 260921-oxm: IMAP-Einstellung STARTTLS erzwingt jetzt wirklich Verschluesselung (die bisher gesetzte Option existiert in imapflow gar nicht), Outlook-Anhaenge werden ueber die Content-Disposition wieder erkannt
Progress: [██████████] 99% Progress: [██████████] 99%
@@ -454,6 +454,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
| 260921-jt4 | **Barrierefreiheit von 30 auf 1 Befund, plus die vier zurueckgestellten Restposten.** Die 30 a11y-Befunde galten seit bi2 als "braucht Bedienentscheidungen"; die hat der Orchestrator getroffen, und der Planer hat **zwei davon widerlegt**: (1) Der vorgesehene Rueckfallweg (`role` + `tabIndex` + Tastaturhandler, wo kein echter Knopf geht) tauscht gemessen drei Befunde gegen einen neuen `useSemanticElements` — eine Regel, die bi2 gerade erst auf 0 gebracht hatte; wird nirgends benutzt, fuer den Verschachtelungsfall (Marktplatz-Karte) tritt eine deckende Geschwister-Schaltflaeche an seine Stelle. (2) **Vier der elf "Klick"-Befunde sind gar keine Klicks**, sondern `onError`-Handler an `<img>` — da gibt es keinen Tastaturweg zu schaffen, sie bekommen `aria-hidden`. **Ein Fund darueber hinaus:** alle fuenf ARIA-Befunde sind `aria-label` auf rollenlosen Elementen — die werden von Vorleseprogrammen still verworfen, die Beschriftungen kamen also bei niemandem an; jetzt mit korrekter Rolle. Sechs Stellen wurden zu echten `<button>` (Aussehen unveraendert), vier `autoFocus` auf Seiten entfernt (auf Seiten reisst er beim Laden den Fokus an sich — im Dialog waere er richtig gewesen, alle vier waren Seiten). **Ein Befund bleibt bewusst stehen und bleibt gezaehlt** (`calculator-widget.tsx:323`), samt ausdruecklich verworfener Umgehung. **Restposten:** ZIP-Name uebersetzt mit getesteter Schutzfunktion `zip-filename.ts` (der frueher genannte Umlaut-Einwand trifft fuer "Zertifikate.zip" nicht zu, die Schutzfunktion sichert kuenftige Uebersetzungen ab); die ueberfluessige `case`-Marke im Normalisierer aufgeloest, Absicht in den Kommentar gewandert; Kalender-Verschwendung abgestellt. **Zur `t`-Frage eine Korrektur an gof:** `use-intl` 4.13 erzeugt `t` in einem `useMemo`, es ist also in der Bibliothek stabil — instabil ist es nur in den Test-Attrappen, und daher kam der Beleg von damals. Die acht Korrekturen aus gof bleiben richtig und schaedlich sind sie nicht, aber die Begruendung war zu breit; ein Test an der Wurzel misst es jetzt. **Laufzeitnachweis vom Orchestrator** (Browser, 90 Tage Vorschau — bei der Voreinstellung 30 tritt der Doppelabruf gar nicht auf, die Messung haette also nichts gezeigt): drei Monatswechsel holen `calendar/sources` nur noch **1x statt 4x**, und der Termin-Abruf mit identischem Zeitraum ist weg (3 Klicks → 2 Abrufe statt 3). Der 5-Minuten-Auffrischer bleibt unangetastet — belegt nicht durch Warten im Browser (zwei Messversuche waren ungueltig, weil das Werkzeug die Seite zwischendurch neu laedt: nach 330 s Wartezeit war das Dokument 37 s alt), sondern durch Test 19 mit gestellter Uhr: nach `advanceTimersByTime(300_000)` werden **beide** Abrufe erneut ausgefuehrt. **Zahlen:** 429 → 399, a11y 30 → 1, web-Tests 69/484 → 73/529, api 72/1143 unveraendert, type-check 4/4, lint 5/5, keine neuen Unterdrueckungen. | 2026-09-21 | a8531d4,3d0bc0b,0c89c13,b601141,e651c24,+7 | [260921-jt4-barrierefreiheit-mit-bedienentscheidunge](./quick/260921-jt4-barrierefreiheit-mit-bedienentscheidunge/) | | 260921-jt4 | **Barrierefreiheit von 30 auf 1 Befund, plus die vier zurueckgestellten Restposten.** Die 30 a11y-Befunde galten seit bi2 als "braucht Bedienentscheidungen"; die hat der Orchestrator getroffen, und der Planer hat **zwei davon widerlegt**: (1) Der vorgesehene Rueckfallweg (`role` + `tabIndex` + Tastaturhandler, wo kein echter Knopf geht) tauscht gemessen drei Befunde gegen einen neuen `useSemanticElements` — eine Regel, die bi2 gerade erst auf 0 gebracht hatte; wird nirgends benutzt, fuer den Verschachtelungsfall (Marktplatz-Karte) tritt eine deckende Geschwister-Schaltflaeche an seine Stelle. (2) **Vier der elf "Klick"-Befunde sind gar keine Klicks**, sondern `onError`-Handler an `<img>` — da gibt es keinen Tastaturweg zu schaffen, sie bekommen `aria-hidden`. **Ein Fund darueber hinaus:** alle fuenf ARIA-Befunde sind `aria-label` auf rollenlosen Elementen — die werden von Vorleseprogrammen still verworfen, die Beschriftungen kamen also bei niemandem an; jetzt mit korrekter Rolle. Sechs Stellen wurden zu echten `<button>` (Aussehen unveraendert), vier `autoFocus` auf Seiten entfernt (auf Seiten reisst er beim Laden den Fokus an sich — im Dialog waere er richtig gewesen, alle vier waren Seiten). **Ein Befund bleibt bewusst stehen und bleibt gezaehlt** (`calculator-widget.tsx:323`), samt ausdruecklich verworfener Umgehung. **Restposten:** ZIP-Name uebersetzt mit getesteter Schutzfunktion `zip-filename.ts` (der frueher genannte Umlaut-Einwand trifft fuer "Zertifikate.zip" nicht zu, die Schutzfunktion sichert kuenftige Uebersetzungen ab); die ueberfluessige `case`-Marke im Normalisierer aufgeloest, Absicht in den Kommentar gewandert; Kalender-Verschwendung abgestellt. **Zur `t`-Frage eine Korrektur an gof:** `use-intl` 4.13 erzeugt `t` in einem `useMemo`, es ist also in der Bibliothek stabil — instabil ist es nur in den Test-Attrappen, und daher kam der Beleg von damals. Die acht Korrekturen aus gof bleiben richtig und schaedlich sind sie nicht, aber die Begruendung war zu breit; ein Test an der Wurzel misst es jetzt. **Laufzeitnachweis vom Orchestrator** (Browser, 90 Tage Vorschau — bei der Voreinstellung 30 tritt der Doppelabruf gar nicht auf, die Messung haette also nichts gezeigt): drei Monatswechsel holen `calendar/sources` nur noch **1x statt 4x**, und der Termin-Abruf mit identischem Zeitraum ist weg (3 Klicks → 2 Abrufe statt 3). Der 5-Minuten-Auffrischer bleibt unangetastet — belegt nicht durch Warten im Browser (zwei Messversuche waren ungueltig, weil das Werkzeug die Seite zwischendurch neu laedt: nach 330 s Wartezeit war das Dokument 37 s alt), sondern durch Test 19 mit gestellter Uhr: nach `advanceTimersByTime(300_000)` werden **beide** Abrufe erneut ausgefuehrt. **Zahlen:** 429 → 399, a11y 30 → 1, web-Tests 69/484 → 73/529, api 72/1143 unveraendert, type-check 4/4, lint 5/5, keine neuen Unterdrueckungen. | 2026-09-21 | a8531d4,3d0bc0b,0c89c13,b601141,e651c24,+7 | [260921-jt4-barrierefreiheit-mit-bedienentscheidunge](./quick/260921-jt4-barrierefreiheit-mit-bedienentscheidunge/) |
| 260921-ldf | **Der Wackeltest war ein echter Produktfehler — nachgewiesen, nicht vermutet.** CI-Lauf 395 war rot; durchgefallen war ein Test aus quick-260914-m97, rund einmal in 17 vollen Laeufen, isoliert nie. Symptom: Vorschaubild da, Haekchen "Bildschirmfoto anhaengen" aus. **Ursache:** der Fehler-melden-Dialog war dauerhaft eingehaengt, sein `useState(screenshot !== null)` lief damit genau einmal — beim allerersten Laden der Seite, als noch kein Bild existierte — und der richtige Wert wurde erst von einem `useEffect` nachgezogen, der bauartbedingt nach dem Commit laeuft. **Beleg, deterministisch statt statistisch:** ein MutationObserver ueber jeden einzelnen DOM-Commit zeigt gegen den alten Stand, ohne jede kuenstliche Verzoegerung: `COMMIT dialog=true img=ja box=AUS` gefolgt von `COMMIT dialog=true img=ja box=AN`. Der falsche Zustand entsteht bei JEDEM Oeffnen, nicht nur unter Last, und haelt zwei Makrotask-Runden — dazwischen darf der Browser zeichnen, ein Nutzer kann es also sehen. **Ehrliche Einordnung der Tragweite:** die Korrektur kommt binnen Millisekunden, lange bevor jemand "Senden" treffen kann. Der befuerchtete Fall (Bild gesehen, abgeschickt, Bild fehlt) ist NICHT erreichbar; es bleibt ein kurzes Flackern. Repariert wurde trotzdem der Produktcode, nicht der Test — wer einen wirklich vorhandenen falschen Zustand im Test wegberuhigt, laesst ihn stehen. **Zwei Teilursachen, einzeln reicht keine:** der Dialog wird nur noch eingehaengt, solange er offen ist (frischer Mount je Oeffnen, der zuruecksetzende Effekt entfaellt), und das Haekchen wird beim Rendern abgeleitet statt nachgezogen. Dieselbe Ursache lag an einer zweiten Stelle: nach einem Versand stand beim erneuten Oeffnen zwei Runden lang der alte Danke-Bildschirm im DOM. **Zur Statistik, weil es der Kern der Sache ist:** 20 volle Laeufe ohne Fehlschlag gelten ausdruecklich NICHT als Beweis — bei der Ausgangsrate 1:17 waeren sie auch ohne Reparatur zu rund 30 Prozent zu erwarten. Tragend ist, dass der falsche Zwischenzustand nicht mehr existiert und die neuen Tests gegen den alten Stand 5 von 5 rot sind. Kein `retry`, kein hoeheres Zeitlimit — die Ursache war nie blosse Zeit. **Zwei Konstruktionsfehler des Tests mitbehoben:** das `expect` innerhalb der Attrappe (wirft es, landet der Fehler mitten im `await` von `captureScreenshot`, dessen `catch` still `null` liefert — der Test waere viel spaeter mit "kein Vorschaubild" durchgefallen, also in die falsche Richtung zeigend) und die per `Object.defineProperty` gesetzte `document.body`-Groesse, die `cleanup()` ueberlebte und alle zwoelf folgenden Tests derselben Datei 3200x1000 sehen liess. **Widerlegt unterwegs:** der Verdacht auf den dynamischen Import von `html-to-image` — er loest auf, bevor ein zuvor gesetzter `setTimeout(0)` feuert, ueberschreitet also keine Makrotask-Grenze. **Zahlen:** Warnungen 399 unveraendert, web-Tests 529 → 531, api 72/1143 unveraendert, type-check 4/4, lint 5/5. | 2026-09-21 | c0ab5b5,de7fdb7,9f02fcc,a6181e2 | [260921-ldf-wackeltest-fehler-melden-haekchen-bildsc](./quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/) | | 260921-ldf | **Der Wackeltest war ein echter Produktfehler — nachgewiesen, nicht vermutet.** CI-Lauf 395 war rot; durchgefallen war ein Test aus quick-260914-m97, rund einmal in 17 vollen Laeufen, isoliert nie. Symptom: Vorschaubild da, Haekchen "Bildschirmfoto anhaengen" aus. **Ursache:** der Fehler-melden-Dialog war dauerhaft eingehaengt, sein `useState(screenshot !== null)` lief damit genau einmal — beim allerersten Laden der Seite, als noch kein Bild existierte — und der richtige Wert wurde erst von einem `useEffect` nachgezogen, der bauartbedingt nach dem Commit laeuft. **Beleg, deterministisch statt statistisch:** ein MutationObserver ueber jeden einzelnen DOM-Commit zeigt gegen den alten Stand, ohne jede kuenstliche Verzoegerung: `COMMIT dialog=true img=ja box=AUS` gefolgt von `COMMIT dialog=true img=ja box=AN`. Der falsche Zustand entsteht bei JEDEM Oeffnen, nicht nur unter Last, und haelt zwei Makrotask-Runden — dazwischen darf der Browser zeichnen, ein Nutzer kann es also sehen. **Ehrliche Einordnung der Tragweite:** die Korrektur kommt binnen Millisekunden, lange bevor jemand "Senden" treffen kann. Der befuerchtete Fall (Bild gesehen, abgeschickt, Bild fehlt) ist NICHT erreichbar; es bleibt ein kurzes Flackern. Repariert wurde trotzdem der Produktcode, nicht der Test — wer einen wirklich vorhandenen falschen Zustand im Test wegberuhigt, laesst ihn stehen. **Zwei Teilursachen, einzeln reicht keine:** der Dialog wird nur noch eingehaengt, solange er offen ist (frischer Mount je Oeffnen, der zuruecksetzende Effekt entfaellt), und das Haekchen wird beim Rendern abgeleitet statt nachgezogen. Dieselbe Ursache lag an einer zweiten Stelle: nach einem Versand stand beim erneuten Oeffnen zwei Runden lang der alte Danke-Bildschirm im DOM. **Zur Statistik, weil es der Kern der Sache ist:** 20 volle Laeufe ohne Fehlschlag gelten ausdruecklich NICHT als Beweis — bei der Ausgangsrate 1:17 waeren sie auch ohne Reparatur zu rund 30 Prozent zu erwarten. Tragend ist, dass der falsche Zwischenzustand nicht mehr existiert und die neuen Tests gegen den alten Stand 5 von 5 rot sind. Kein `retry`, kein hoeheres Zeitlimit — die Ursache war nie blosse Zeit. **Zwei Konstruktionsfehler des Tests mitbehoben:** das `expect` innerhalb der Attrappe (wirft es, landet der Fehler mitten im `await` von `captureScreenshot`, dessen `catch` still `null` liefert — der Test waere viel spaeter mit "kein Vorschaubild" durchgefallen, also in die falsche Richtung zeigend) und die per `Object.defineProperty` gesetzte `document.body`-Groesse, die `cleanup()` ueberlebte und alle zwoelf folgenden Tests derselben Datei 3200x1000 sehen liess. **Widerlegt unterwegs:** der Verdacht auf den dynamischen Import von `html-to-image` — er loest auf, bevor ein zuvor gesetzter `setTimeout(0)` feuert, ueberschreitet also keine Makrotask-Grenze. **Zahlen:** Warnungen 399 unveraendert, web-Tests 529 → 531, api 72/1143 unveraendert, type-check 4/4, lint 5/5. | 2026-09-21 | c0ab5b5,de7fdb7,9f02fcc,a6181e2 | [260921-ldf-wackeltest-fehler-melden-haekchen-bildsc](./quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/) |
| 260921-m34 | **288 `any` im Backend beurteilt: 15 bleiben, mit Urteil je Stelle.** Drei Durchgaenge. **Der groesste Posten war ein einziges Missverstaendnis:** 105 Stellen trugen `forTenant(...) as any`, obwohl `prisma.$extends()` laengst einen getypten Klienten liefert — die Zusicherung war nie noetig. Entfernen ergab genau EINEN Folgefehler, und der war selbst ein Befund (eine Handannotation, die nur existierte, um unter dem ungetypten Klienten eine Meldung zu umgehen, und falsch geworden war). **Aufgabe 2 war die sicherheitsrelevante:** ein gemeinsamer Typ `AuthUser` fuer die Aufrufer-Identitaet. Die `tenantId`-Frage wurde HERGELEITET, nicht nach Bequemlichkeit entschieden — `string | undefined` erzeugt 8 Fehler, `string` keinen, und das war ausdruecklich kein Argument. Belege: Pflichtspalte in `schema.prisma:38`, Bestandstyp `SessionUser`, und der Super-Admin-Zweig in `TenantGuard`. Der dritte Beleg widerlegt `string` NICHT, weil der Waechter sein Anfrageobjekt ungetypt holt und `AuthUser` gar nicht liest — der Zweig kann also nicht zu totem Code werden. Dass es ihn gibt, steht trotzdem im Typsystem: `AuthenticatedRequest.tenantId` ist `string | null | undefined`, das `null` stammt nur von dort, mit Warnkommentar. `tenant.guard.ts` ueber den ganzen Lauf 0 geaenderte Zeilen (Tor). **Aufgabe 3 ist zugleich das Urteilsregister:** typisiert 252, auf `unknown` umgestellt 21, bleibt 15 — jede der 15 mit Begruendung im Code (6 node-forge, wo die mitgelieferten Typen die Bibliothek nachweislich falsch beschreiben; 3 Cron; 4 `withTenantTransaction`, wo der genaue Typ eine bewusst unvollstaendige Test-Attrappe braeche; 2 imapflow). Null war ausdruecklich NICHT das Ziel. **Vier Befunde gemeldet statt still repariert** — zwei davon brauchen eine Entscheidung des Nutzers: (B-06, sicherheitsrelevant) `imap.provider.ts:402` setzt `requireTLS`, das es in imapflow 1.4.3 NIRGENDS gibt (vom Orchestrator unabhaengig nachgeprueft: kein Treffer im ganzen Paket). Die Option wird still verworfen, die Einstellung "STARTTLS" erzwingt also nichts; die Bibliothek faellt dann auf ihr Standardverhalten zurueck und setzt laut eigener Dokumentation unverschluesselt fort, wenn der Server kein STARTTLS anbietet — sie nennt das selbst eine Downgrade-Angriffsflaeche. Richtig waere `doSTARTTLS: true`. Die `as any`-Zusicherung hatte das verdeckt. (B-05) `imap.provider.ts:78` liest `.parameters` von einer Zeichenkette (imapflow deklariert `disposition: string`, die Parameter liegen in `dispositionParameters`) — zur Laufzeit immer `undefined`, Outlook-Anhaenge als `application/octet-stream` werden ueber Content-Disposition nicht erkannt; betrifft den DKV-Rechnungseinzug. Dazu (B-04) eine Falle im RLS-Erkenner (er zaehlt jede `select:`-Angabe ausserhalb eines Modellaufrufs als Verstoss) — Erkenner NICHT aufgeweicht, Typ anders hergeleitet; und (B-07) httpntlm liefert den Rumpf als Zeichenkette, nicht als Buffer. **Zahlen:** Diagnosen 399 → 125, `any` im Quellcode 288 → 15, `apps/web` 1 → 0, Disziplin-Zaehler unveraendert (`as unknown as` 33, `noNonNullAssertion` 56, Unterdrueckungen 1, `ts-expect-error` 0), api 72/1143, web 73/531, type-check 4/4, lint 5/5, RLS-Waechter 30/30. | 2026-09-21 | b188946,f2fc39f,7c9d7c1,52668c2,32591b6,3892c5f,d8fb9ae | [260921-m34-288-any-im-backend-einzeln-beurteilen-un](./quick/260921-m34-288-any-im-backend-einzeln-beurteilen-un/) | | 260921-m34 | **288 `any` im Backend beurteilt: 15 bleiben, mit Urteil je Stelle.** Drei Durchgaenge. **Der groesste Posten war ein einziges Missverstaendnis:** 105 Stellen trugen `forTenant(...) as any`, obwohl `prisma.$extends()` laengst einen getypten Klienten liefert — die Zusicherung war nie noetig. Entfernen ergab genau EINEN Folgefehler, und der war selbst ein Befund (eine Handannotation, die nur existierte, um unter dem ungetypten Klienten eine Meldung zu umgehen, und falsch geworden war). **Aufgabe 2 war die sicherheitsrelevante:** ein gemeinsamer Typ `AuthUser` fuer die Aufrufer-Identitaet. Die `tenantId`-Frage wurde HERGELEITET, nicht nach Bequemlichkeit entschieden — `string | undefined` erzeugt 8 Fehler, `string` keinen, und das war ausdruecklich kein Argument. Belege: Pflichtspalte in `schema.prisma:38`, Bestandstyp `SessionUser`, und der Super-Admin-Zweig in `TenantGuard`. Der dritte Beleg widerlegt `string` NICHT, weil der Waechter sein Anfrageobjekt ungetypt holt und `AuthUser` gar nicht liest — der Zweig kann also nicht zu totem Code werden. Dass es ihn gibt, steht trotzdem im Typsystem: `AuthenticatedRequest.tenantId` ist `string | null | undefined`, das `null` stammt nur von dort, mit Warnkommentar. `tenant.guard.ts` ueber den ganzen Lauf 0 geaenderte Zeilen (Tor). **Aufgabe 3 ist zugleich das Urteilsregister:** typisiert 252, auf `unknown` umgestellt 21, bleibt 15 — jede der 15 mit Begruendung im Code (6 node-forge, wo die mitgelieferten Typen die Bibliothek nachweislich falsch beschreiben; 3 Cron; 4 `withTenantTransaction`, wo der genaue Typ eine bewusst unvollstaendige Test-Attrappe braeche; 2 imapflow). Null war ausdruecklich NICHT das Ziel. **Vier Befunde gemeldet statt still repariert** — zwei davon brauchen eine Entscheidung des Nutzers: (B-06, sicherheitsrelevant) `imap.provider.ts:402` setzt `requireTLS`, das es in imapflow 1.4.3 NIRGENDS gibt (vom Orchestrator unabhaengig nachgeprueft: kein Treffer im ganzen Paket). Die Option wird still verworfen, die Einstellung "STARTTLS" erzwingt also nichts; die Bibliothek faellt dann auf ihr Standardverhalten zurueck und setzt laut eigener Dokumentation unverschluesselt fort, wenn der Server kein STARTTLS anbietet — sie nennt das selbst eine Downgrade-Angriffsflaeche. Richtig waere `doSTARTTLS: true`. Die `as any`-Zusicherung hatte das verdeckt. (B-05) `imap.provider.ts:78` liest `.parameters` von einer Zeichenkette (imapflow deklariert `disposition: string`, die Parameter liegen in `dispositionParameters`) — zur Laufzeit immer `undefined`, Outlook-Anhaenge als `application/octet-stream` werden ueber Content-Disposition nicht erkannt; betrifft den DKV-Rechnungseinzug. Dazu (B-04) eine Falle im RLS-Erkenner (er zaehlt jede `select:`-Angabe ausserhalb eines Modellaufrufs als Verstoss) — Erkenner NICHT aufgeweicht, Typ anders hergeleitet; und (B-07) httpntlm liefert den Rumpf als Zeichenkette, nicht als Buffer. **Zahlen:** Diagnosen 399 → 125, `any` im Quellcode 288 → 15, `apps/web` 1 → 0, Disziplin-Zaehler unveraendert (`as unknown as` 33, `noNonNullAssertion` 56, Unterdrueckungen 1, `ts-expect-error` 0), api 72/1143, web 73/531, type-check 4/4, lint 5/5, RLS-Waechter 30/30. | 2026-09-21 | b188946,f2fc39f,7c9d7c1,52668c2,32591b6,3892c5f,d8fb9ae | [260921-m34-288-any-im-backend-einzeln-beurteilen-un](./quick/260921-m34-288-any-im-backend-einzeln-beurteilen-un/) |
| 260921-oxm | **IMAP: STARTTLS erzwingt jetzt wirklich, Outlook-Anhaenge werden erkannt.** Die zwei Befunde aus m34, beide mit Entscheidung des Nutzers behoben. **(B-06, Sicherheit)** `imap.provider.ts` setzte `requireTLS` — eine Option, die es in imapflow 1.4.3 NIRGENDS gibt (Orchestrator: kein Treffer im ganzen Paket). Sie wurde still verworfen, die Bibliothek fiel auf ihr Standardverhalten zurueck und setzte laut eigener Dokumentation unverschluesselt fort, wenn der Server kein STARTTLS anbietet — Benutzername und Kennwort gingen dann im Klartext. Ersetzt durch `doSTARTTLS`, nachgeprueft in `imap-flow.d.ts:81` und `imap-flow.js:1183`. Bei implizitem TLS wird ausdruecklich `false` gesetzt, nicht weggelassen: die Bibliothek wirft bei `secure=true` zusammen mit `doSTARTTLS=true`. **Gewollte Verhaltensaenderung:** ein auf STARTTLS eingestelltes Postfach, dessen Server das nicht anbietet, meldet ab jetzt einen Verbindungsfehler statt still im Klartext zu verbinden. **(B-05)** `imap.provider.ts:78` las `.parameters` von einer Zeichenkette — imapflow fuehrt die Parameter in `dispositionParameters` (`imap-flow.d.ts:450`), der Ausdruck war zur Laufzeit immer leer. Anhaenge als `application/octet-stream` (typisch Outlook) wurden ueber die Content-Disposition nicht erkannt; betraf den DKV-Rechnungseinzug. Nachgeprueft: imapflow schreibt die Schluessel klein und setzt RFC-2231-Fortsetzungen selbst zusammen — dafuer war nichts zu tun. **Rot-dann-gruen belegt:** gegen den Stand mit Tests aber ohne Reparatur scheiterten genau 3 von 12 Faellen, danach 12/12. Zwei der fuenf neuen Faelle sind absichtlich von Anfang an gruen — sie sichern ab, dass B-05 nicht zu viel einsammelt. **Die `as any`-Zusicherung konnte ersatzlos entfallen** (sie existierte nur wegen der erfundenen Option); alle sechs uebergebenen Felder sind jetzt deklariert. **Zahlen:** `any` im Backend 15 → 13, `as unknown as` 33 → 27 (Testdoppel-Einhaengung in einen Helfer gezogen statt fuenf neue Umdeutungen), kein Zaehler gestiegen, api-Tests 1143 → 1148, web 73/531, type-check 4/4, lint 5/5. | 2026-09-21 | 7691d1f,d0266bf,6def539 | [260921-oxm-imap-starttls-wirklich-erzwingen-und-anh](./quick/260921-oxm-imap-starttls-wirklich-erzwingen-und-anh/) |
## Deferred Items ## Deferred Items
@@ -499,4 +500,4 @@ Last session: 2026-09-21T04:50:00Z
Resumed: 2026-09-21 — Sitzung ueber /gsd-resume-work fortgesetzt. Stand geprueft: Arbeitsbaum sauber, main == origin/main auf 55aa287, CI-Lauf 387 fuer 55aa287 erfolgreich (Beta-Images gebaut). Push und CI aus dem letzten Stopp-Punkt sind damit erledigt. Resumed: 2026-09-21 — Sitzung ueber /gsd-resume-work fortgesetzt. Stand geprueft: Arbeitsbaum sauber, main == origin/main auf 55aa287, CI-Lauf 387 fuer 55aa287 erfolgreich (Beta-Images gebaut). Push und CI aus dem letzten Stopp-Punkt sind damit erledigt.
Stopped at: Warte auf Nutzerentscheidung, womit weitergearbeitet wird. Offen fuer den User: alpha pullen (web+api) und danach am Windows-VM-Client die echte Fehlermeldung schicken (Betreff `[Desktop/Windows]` + `Herkunft:`-Zeile pruefen); eigenen Arbeitsplatz-Client einmal per Browser-Installer erneuern; Freigabe 1.3.0 auf Zuruf. Technisch offen im Ledger: WINDOWS #35 (Biome laeuft nicht — biome.json:3 `organizeImports` ist in Biome 2.5.0 unbekannt, `biome check` bricht mit Konfigurationsfehler ab, reproduziert 2026-09-21) und WINDOWS #36 (403-Antworten bleiben in handleSubmit/handleDelete ohne sichtbare Reaktion). Stopped at: Warte auf Nutzerentscheidung, womit weitergearbeitet wird. Offen fuer den User: alpha pullen (web+api) und danach am Windows-VM-Client die echte Fehlermeldung schicken (Betreff `[Desktop/Windows]` + `Herkunft:`-Zeile pruefen); eigenen Arbeitsplatz-Client einmal per Browser-Installer erneuern; Freigabe 1.3.0 auf Zuruf. Technisch offen im Ledger: WINDOWS #35 (Biome laeuft nicht — biome.json:3 `organizeImports` ist in Biome 2.5.0 unbekannt, `biome check` bricht mit Konfigurationsfehler ab, reproduziert 2026-09-21) und WINDOWS #36 (403-Antworten bleiben in handleSubmit/handleDelete ohne sichtbare Reaktion).
Resume file: None Resume file: None
Last activity: 2026-09-21 - Quick 260921-m34: 288 any im Backend auf 15 gesenkt, jede verbliebene mit Urteil; dabei vier Befunde gemeldet statt still repariert, darunter ein sicherheitsrelevanter: die IMAP-Einstellung STARTTLS erzwingt nichts, weil die gesetzte Option in imapflow gar nicht existiert Last activity: 2026-09-21 - Quick 260921-oxm: IMAP-Einstellung STARTTLS erzwingt jetzt wirklich Verschluesselung (die bisher gesetzte Option existiert in imapflow gar nicht), Outlook-Anhaenge werden ueber die Content-Disposition wieder erkannt
@@ -0,0 +1,194 @@
---
phase: quick-260921-oxm
plan: 01
subsystem: apps/api/src/inbox
tags: [imap, imapflow, starttls, sicherheit, anhaenge, dkv, tdd]
status: complete
requires:
- "260921-m34 (Befunde B-05 und B-06)"
provides:
- "ImapProvider erzwingt STARTTLS ueber die Option, die imapflow wirklich kennt"
- "ImapProvider erkennt Anhangs-Dateinamen aus Content-Disposition"
affects:
- "apps/api/src/inbox/imap.provider.ts"
- "apps/api/src/inbox/imap.provider.spec.ts"
tech-stack:
added: []
patterns:
- "Konstruktoroptionen einer Fremdbibliothek im Test gegen die uebergebenen Werte pruefen, nicht gegen eine echte Verbindung"
key-files:
created: []
modified:
- apps/api/src/inbox/imap.provider.ts
- apps/api/src/inbox/imap.provider.spec.ts
decisions:
- "doSTARTTLS bei ssl-tls auf false statt weglassen: schliesst die Unvertraeglichkeit secure=true + doSTARTTLS=true aus und schaltet STARTTLS dort ausdruecklich ab"
- "Einhaengen des Testdoppels in einen Helfer gezogen, damit die neuen Faelle ohne eigene Umdeutung auskommen"
metrics:
duration: "35 min (17:55 bis 18:30 Uhr, 21.09.2026)"
completed: 2026-09-21
actuals:
tokens: 31000
tasks: 2
commits: 4
plan_head_before: ad83407
---
# Quick-Aufgabe 260921-oxm: IMAP-STARTTLS wirklich erzwingen und Anhangs-Dateinamen richtig lesen Summary
Zwei falsche Annahmen im IMAP-Postfachzugriff sind behoben: die Einstellung
"STARTTLS" bewirkt jetzt tatsaechlich, was ihr Name verspricht, und
Rechnungsanhaenge aus Outlook werden wieder am Dateinamen erkannt. Beide
Reparaturen haengen an Tests, die gegen den vorherigen Stand nachweislich rot
waren.
## Was sich fuer den Betrieb aendert
**Die eine gewollte Verhaltensaenderung, in einem Satz:** Ein Postfach, das auf
"STARTTLS" eingestellt ist, dessen Server diese Verschluesselung aber gar nicht
anbietet, meldet ab jetzt einen Verbindungsfehler — bisher hat Tessera in genau
diesem Fall stillschweigend unverschluesselt weitergemacht und Benutzername und
Kennwort im Klartext uebertragen. Wer so ein Postfach hat, sieht den Fehler
sofort und kann die Einstellung richtigstellen; vorher hat niemand etwas
gemerkt.
**Die zweite Aenderung ist eine Reparatur, keine Umstellung:** Anhaenge, die ein
Absender als `application/octet-stream` verschickt — was Outlook regelmaessig
tut — wurden bisher nur dann als PDF erkannt, wenn der Dateiname zusaetzlich im
Inhaltstyp stand. Der zweite, haeufigere Weg ueber die Angabe
"Content-Disposition" wurde zwar abgefragt, lieferte aber baulich bedingt nie
ein Ergebnis. Er funktioniert jetzt. Betrifft den DKV-Rechnungseinzug.
## B-06 — STARTTLS wurde nie erzwungen
`buildClient()` uebergab `requireTLS: config.encryption === 'starttls'` an
`new ImapFlow(...)`. Diese Option kennt imapflow 1.4.3 nicht: weder
`ImapFlowOptions` in `lib/imap-flow.d.ts` noch der Laufzeitcode in
`lib/imap-flow.js` erwaehnen sie — beides durchsucht, kein einziger Treffer. Sie
wurde also entgegengenommen und weggeworfen. Verdeckt hat das die Zusicherung
`} as any` am Ende derselben Funktion: sie hat dem Compiler verboten, die
unbekannte Option zu bemaengeln.
Ohne gesetzte Option galt das Standardverhalten der Bibliothek, das sie selbst
so beschreibt: bei `secure=false` auf TLS hochstufen, *falls* der Server es
anbietet, sonst unverschluesselt weitermachen — mit dem ausdruecklichen Zusatz
*"This may expose the connection to a downgrade attack."*
**Reparatur:** `doSTARTTLS: config.encryption === 'starttls'` (deklariert in
`imap-flow.d.ts:81`, ausgewertet in `imap-flow.js:1183`). Bei `starttls` ergibt
der Ausdruck `true` und die Verbindung scheitert, wenn der Server kein STARTTLS
kann. Bei `ssl-tls` ergibt er `false`, was STARTTLS ausdruecklich abschaltet
(`imap-flow.js:1210`) — das ist wichtiger als es aussieht: die Bibliothek wirft
bei `secure=true` zusammen mit `doSTARTTLS=true` einen Konfigurationsfehler
(`imap-flow.js:1201`). Ein schlichtes `true`/`undefined` waere hier also falsch
gewesen. Genau diese Kombination prueft der zweite Test mit.
**Die Zusicherung konnte ersatzlos entfallen.** Nach der Reparatur sind alle
sechs uebergebenen Felder in `ImapFlowOptions` deklariert; `tsc` ist ohne das
`as any` fehlerfrei. Damit ist die Stelle nicht nur getypt, sondern kann kuenftig
auch keine weitere erfundene Option mehr verstecken.
## B-05 — der Dateiname kam aus dem falschen Feld
`collectPdfParts()` las `(node as any).disposition?.parameters?.filename`.
imapflow deklariert `disposition` aber als **Zeichenkette**
(`imap-flow.d.ts:448` — der Wert ist "attachment" oder "inline") und legt die
zugehoerigen Parameter in ein eigenes Feld `dispositionParameters`
(`imap-flow.d.ts:450`). Der Ausdruck las also `.parameters` von einer
Zeichenkette und war zur Laufzeit **immer** `undefined`.
**Reparatur:** `node.dispositionParameters?.filename?.toLowerCase() ?? ''` —
getypt, ohne Zusicherung. Nachgeprueft, nicht geraten: imapflow fuellt das Feld
in `tools.js:887` ueber `getStructuredParams()`, und diese Funktion schreibt die
Schluessel **kleingeschrieben** (`tools.js:648`). `filename` ist damit der
richtige Schluessel, unabhaengig davon, wie der Absender die Angabe gross- oder
kleingeschrieben hat.
Der zweite moegliche Fundort, den die Aufgabe erwaehnt — der Name in den
Parametern des Inhaltstyps — war bereits vorhanden und wird weiter geprueft
(`node.parameters?.name`). Ein dritter Fall wurde nicht erfunden. Die
RFC-2231-Fortsetzungsparameter (`filename*0`, `filename*1` ...) setzt imapflow
selbst wieder zu einem einzigen `filename` zusammen (`tools.js:662` ff.), es
braucht dafuer hier also nichts.
## Die Tests, und der Beleg dass sie rot waren
Beide Faelle liegen in `apps/api/src/inbox/imap.provider.spec.ts`. Gegen den
Stand `7691d1f` (Tests vorhanden, Reparatur noch nicht) scheiterten genau drei
von zwoelf Faellen:
```
× ImapProvider - Transportverschluesselung (B-06) > erzwingt STARTTLS, wenn die Verschluesselung auf starttls steht
-> expected undefined to be true // Object.is equality
× ImapProvider - Transportverschluesselung (B-06) > setzt doSTARTTLS nicht auf true, wenn die Verschluesselung auf ssl-tls steht
-> expected { host: 'imap.example.com', ...(5) } to not have property "requireTLS"
× ImapProvider.fetchPdfAttachments - Dateiname aus Content-Disposition (B-05) > erkennt einen application/octet-stream-Anhang am Dateinamen aus dispositionParameters
-> expected [] to have a length of 1 but got +0
Test Files 1 failed (1)
Tests 3 failed | 9 passed (12)
```
Die erste Zeile ist der Kern von B-06: `doSTARTTLS` war schlicht nicht gesetzt.
Die zweite belegt, dass stattdessen ein Feld `requireTLS` ankam, das die
Bibliothek nicht auswertet. Die dritte belegt B-06 nicht, sondern B-05: der
Anhang wurde gar nicht erst eingesammelt.
Zwei weitere neue Faelle waren von Anfang an gruen und sollen das auch bleiben —
sie sichern, dass die Erkennung ueber den Inhaltstyp-Namen weiter greift und
dass ein `octet-stream`-Anhang **ohne** `.pdf`-Endung weiterhin liegen bleibt.
Ohne sie haette die Reparatur unbemerkt zu viel einsammeln koennen.
Nach der Reparatur: 12 von 12 gruen.
## Messungen
| Groesse | vorher (ad83407) | nachher (d0266bf) |
|---|---:|---:|
| `lint/suspicious/noExplicitAny` in `apps/api/src` | 15 | **13** |
| `lint/style/noNonNullAssertion` in `apps/api/src` | 56 | 56 |
| `as unknown as` in `apps/api/src` | 33 | **27** |
| `biome-ignore` in `apps/api/src` | 1 | 1 |
| `ts-expect-error` / `@ts-ignore` | 0 | 0 |
| `pnpm type-check` | 4/4 | 4/4 |
| `pnpm lint` | 5/5, 0 Fehler | 5/5, 0 Fehler |
| Tests `apps/api` | 72 Dateien / 1143 | 72 Dateien / **1148** |
| Tests `apps/web` | 73 / 531 | 73 / 531 |
Zwei Zeilen brauchen eine Erklaerung.
**`noExplicitAny` 15 auf 13:** beide verbliebenen imapflow-Stellen aus dem
Urteilsregister von 260921-m34 (Nummern 14 und 15) sind weg. Die 13
verbleibenden sind unveraendert die dort begruendeten: sechs an node-forge, drei
an der Cron-Beschaffung, vier an der Transaktionshilfe.
**`as unknown as` 33 auf 27:** das ist keine Nebenwirkung der Reparatur, sondern
Absicht. Die Testdatei haengte ihr Testdoppel in jedem einzelnen Fall mit
derselben Umdeutung des Konstruktors ein — zwoelf Mal, sobald die neuen Faelle
dazukamen. Diese eine Zeile steht jetzt in einem Helfer `useMockClient()`, und
die neuen Faelle brauchen keine eigene Umdeutung mehr. Die Alternative waere
gewesen, fuenf neue Umdeutungen hinzuzufuegen und den Zaehler zu heben; das war
ausgeschlossen. Der Zaehler faellt, er steigt an keiner Stelle.
## Deviations from Plan
Eine, und sie steht schon oben: der Helfer `useMockClient()` in der Testdatei
war im Plan nicht vorgesehen. Er wurde noetig, weil die neuen Faelle das
Testdoppel sonst nur ueber fuenf zusaetzliche Umdeutungen haetten einhaengen
koennen — was die Vorgabe "Zaehler duerfen nicht steigen" verletzt haette. Die
Aenderung ist mechanisch (dieselbe Zeile, an einer Stelle statt an zwoelf) und
aendert an keinem bestehenden Fall das Verhalten; alle sieben Altfaelle sind
unveraendert gruen.
Sonst nichts: keine neue Abhaengigkeit, kein Versionssprung, kein repo-weites
Umformatieren, keine Aenderung an `STATE.md` oder `ROADMAP.md`.
## Known Stubs
Keine.
## Self-Check: PASSED
Alle vier genannten Dateien liegen auf der Platte, alle drei Commits sind in
`git log` auffindbar (f23671a, 7691d1f, d0266bf). Die Messungen der Tabelle oben
stammen aus tatsaechlich gelaufenen Befehlen, nicht aus einer Schaetzung.
+147 -7
View File
@@ -72,6 +72,17 @@ function makeMockClient(overrides: Partial<Record<string, unknown>> = {}) {
}; };
} }
/**
* Haengt ein Testdoppel als ImapFlow-Klient ein.
*
* Der Modul-Mock oben ersetzt den Konstruktor durch `vi.fn()`; diese Funktion
* ist die einzige Stelle im Test, die das ausnutzt. Vorher stand dieselbe
* Umdeutung in jedem einzelnen Fall.
*/
function useMockClient(client: ReturnType<typeof makeMockClient>): void {
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
}
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@@ -79,7 +90,7 @@ beforeEach(() => {
describe('ImapProvider.fetchMessages', () => { describe('ImapProvider.fetchMessages', () => {
it('returns one InboxMessage with bodyHtml from the html part and bodyText from the plain part', async () => { it('returns one InboxMessage with bodyHtml from the html part and bodyText from the plain part', async () => {
const client = makeMockClient(); const client = makeMockClient();
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
const messages = await provider.fetchMessages(BASE_CONFIG); const messages = await provider.fetchMessages(BASE_CONFIG);
@@ -97,7 +108,7 @@ describe('ImapProvider.fetchMessages', () => {
it('marks each processed message \\Seen (idempotency for re-polls)', async () => { it('marks each processed message \\Seen (idempotency for re-polls)', async () => {
const client = makeMockClient(); const client = makeMockClient();
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
await provider.fetchMessages(BASE_CONFIG); await provider.fetchMessages(BASE_CONFIG);
@@ -107,7 +118,7 @@ describe('ImapProvider.fetchMessages', () => {
it('honors the same UNSEEN + optional senderFilter search as fetchPdfAttachments', async () => { it('honors the same UNSEEN + optional senderFilter search as fetchPdfAttachments', async () => {
const client = makeMockClient(); const client = makeMockClient();
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
await provider.fetchMessages({ ...BASE_CONFIG, senderFilter: 'vergabeportal.de' }); await provider.fetchMessages({ ...BASE_CONFIG, senderFilter: 'vergabeportal.de' });
@@ -122,7 +133,7 @@ describe('ImapProvider.fetchMessages', () => {
const client = makeMockClient({ const client = makeMockClient({
connect: vi.fn().mockRejectedValue(new Error('ECONNREFUSED')), connect: vi.fn().mockRejectedValue(new Error('ECONNREFUSED')),
}); });
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
await expect(provider.fetchMessages(BASE_CONFIG)).resolves.toEqual([]); await expect(provider.fetchMessages(BASE_CONFIG)).resolves.toEqual([]);
@@ -132,7 +143,7 @@ describe('ImapProvider.fetchMessages', () => {
const client = makeMockClient({ const client = makeMockClient({
search: vi.fn().mockRejectedValue(new Error('search boom')), search: vi.fn().mockRejectedValue(new Error('search boom')),
}); });
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
await expect(provider.fetchMessages(BASE_CONFIG)).resolves.toEqual([]); await expect(provider.fetchMessages(BASE_CONFIG)).resolves.toEqual([]);
@@ -141,7 +152,7 @@ describe('ImapProvider.fetchMessages', () => {
it('returns [] when there are no unread messages', async () => { it('returns [] when there are no unread messages', async () => {
const client = makeMockClient({ search: vi.fn().mockResolvedValue([]) }); const client = makeMockClient({ search: vi.fn().mockResolvedValue([]) });
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
const messages = await provider.fetchMessages(BASE_CONFIG); const messages = await provider.fetchMessages(BASE_CONFIG);
@@ -164,7 +175,7 @@ describe('ImapProvider.fetchMessages', () => {
}, },
]), ]),
}); });
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client); useMockClient(client);
const provider = new ImapProvider(); const provider = new ImapProvider();
const messages = await provider.fetchMessages(BASE_CONFIG); const messages = await provider.fetchMessages(BASE_CONFIG);
@@ -174,3 +185,132 @@ describe('ImapProvider.fetchMessages', () => {
expect(messages[0]!.bodyText).toBe(''); expect(messages[0]!.bodyText).toBe('');
}); });
}); });
/**
* Befund B-06 (gemeldet in 260921-m34): buildClient() uebergab `requireTLS`,
* eine Option, die imapflow 1.4.3 gar nicht kennt — weder in `ImapFlowOptions`
* (lib/imap-flow.d.ts) noch im Laufzeitcode (lib/imap-flow.js). Sie wurde still
* verworfen, ein STARTTLS-Zwang entstand durch sie nie. Die richtige Option
* heisst `doSTARTTLS` (imap-flow.d.ts:81).
*
* Geprueft wird hier ausschliesslich, was an `new ImapFlow(...)` uebergeben
* wird — keine echte Verbindung.
*/
describe('ImapProvider — Transportverschluesselung (B-06)', () => {
/** Optionen des zuletzt erzeugten ImapFlow-Klienten. */
function lastClientOptions() {
const calls = vi.mocked(ImapFlow).mock.calls;
return calls[calls.length - 1]?.[0];
}
it('erzwingt STARTTLS, wenn die Verschluesselung auf starttls steht', async () => {
const client = makeMockClient();
useMockClient(client);
const provider = new ImapProvider();
await provider.testConnection({ ...BASE_CONFIG, port: 143, encryption: 'starttls' });
const options = lastClientOptions();
expect(options?.secure).toBe(false);
expect(options?.doSTARTTLS).toBe(true);
expect(options).not.toHaveProperty('requireTLS');
});
it('setzt doSTARTTLS nicht auf true, wenn die Verschluesselung auf ssl-tls steht', async () => {
const client = makeMockClient();
useMockClient(client);
const provider = new ImapProvider();
await provider.testConnection({ ...BASE_CONFIG, encryption: 'ssl-tls' });
const options = lastClientOptions();
// imapflow wirft bei secure=true zusammen mit doSTARTTLS=true
// ("Misconfiguration", imap-flow.js:1201) — diese Kombination darf nie entstehen.
expect(options?.secure).toBe(true);
expect(options?.doSTARTTLS).not.toBe(true);
expect(options).not.toHaveProperty('requireTLS');
});
});
/**
* Befund B-05 (gemeldet in 260921-m34): der Dateiname aus Content-Disposition
* wurde als `disposition.parameters.filename` gelesen. imapflow deklariert
* `disposition` aber als Zeichenkette (imap-flow.d.ts:448) und legt die
* Parameter in ein eigenes Feld `dispositionParameters` (:450, gefuellt in
* tools.js:887 mit kleingeschriebenen Schluesseln). Der alte Ausdruck war zur
* Laufzeit immer undefined — Anhaenge, die als application/octet-stream
* ankommen (typisch fuer Outlook), wurden darueber nie erkannt.
*/
describe('ImapProvider.fetchPdfAttachments — Dateiname aus Content-Disposition (B-05)', () => {
function makeAttachmentClient(bodyStructure: unknown) {
return makeMockClient({
search: vi.fn().mockResolvedValue([7]),
fetchAll: vi.fn().mockResolvedValue([
{
uid: 7,
envelope: {
messageId: '<msg-7@example.com>',
subject: 'Rechnung',
from: [{ address: 'rechnung@dkv.de' }],
date: new Date('2026-07-20T08:00:00Z'),
},
bodyStructure,
},
]),
download: vi.fn(async () => ({ content: makeReadable('%PDF-1.4 inhalt') })),
});
}
it('erkennt einen application/octet-stream-Anhang am Dateinamen aus dispositionParameters', async () => {
const client = makeAttachmentClient({
type: 'multipart/mixed',
childNodes: [
{ type: 'text/plain', part: '1' },
{
type: 'application/octet-stream',
part: '2',
disposition: 'attachment',
dispositionParameters: { filename: 'Rechnung-4711.PDF' },
},
],
});
useMockClient(client);
const provider = new ImapProvider();
const emails = await provider.fetchPdfAttachments(BASE_CONFIG);
expect(emails).toHaveLength(1);
expect(emails[0]?.attachments ?? []).toHaveLength(1);
expect(emails[0]?.attachments?.[0]?.contentType).toBe('application/pdf');
});
it('erkennt einen application/octet-stream-Anhang weiterhin am Namen aus Content-Type', async () => {
const client = makeAttachmentClient({
type: 'application/octet-stream',
part: '1',
parameters: { name: 'Rechnung-4711.pdf' },
});
useMockClient(client);
const provider = new ImapProvider();
const emails = await provider.fetchPdfAttachments(BASE_CONFIG);
expect(emails).toHaveLength(1);
expect(emails[0]?.attachments ?? []).toHaveLength(1);
});
it('sammelt einen application/octet-stream-Anhang ohne .pdf-Dateinamen nicht ein', async () => {
const client = makeAttachmentClient({
type: 'application/octet-stream',
part: '1',
disposition: 'attachment',
dispositionParameters: { filename: 'Lieferschein.docx' },
});
useMockClient(client);
const provider = new ImapProvider();
const emails = await provider.fetchPdfAttachments(BASE_CONFIG);
expect(emails).toEqual([]);
});
});
+18 -25
View File
@@ -62,20 +62,14 @@ function collectPdfParts(
const type = node.type?.toLowerCase() ?? ''; const type = node.type?.toLowerCase() ?? '';
// Some mail clients (e.g. Outlook) send PDFs as application/octet-stream. // Some mail clients (e.g. Outlook) send PDFs as application/octet-stream.
// Fall back to checking the filename from Content-Disposition or Content-Type parameters. // Fall back to checking the filename from Content-Disposition or Content-Type parameters.
// BLEIBT als any, mit Befund (260921-m34, Aufgabe 3c, D-01/D-03): // Repariert in 260921-oxm (Befund B-05 aus 260921-m34): hier stand zuvor
// imapflow deklariert `disposition` als ZEICHENKETTE (imap-flow.d.ts:448, // `disposition?.parameters?.filename`, auf einem zu any umgedeuteten Knoten.
// also "attachment"/"inline"), und die zugehoerigen Parameter liegen in // imapflow deklariert `disposition` aber als ZEICHENKETTE (imap-flow.d.ts:448, also
// einem eigenen Feld `dispositionParameters` (:450). Der Ausdruck unten // "attachment"/"inline") und legt die zugehoerigen Parameter in ein eigenes
// liest `.parameters` von einer Zeichenkette und ist damit zur Laufzeit // Feld `dispositionParameters` (:450, gefuellt in tools.js:887 mit
// IMMER undefined — dispositionFilename ist stets ''. Das ist ein Befund // kleingeschriebenen Schluesseln). Der alte Ausdruck las `.parameters` von
// im Bestandscode, kein Typproblem: ihn hier auf `dispositionParameters` // einer Zeichenkette und war zur Laufzeit IMMER undefined.
// umzubiegen waere eine Verhaltensaenderung (Outlook-Anhaenge als const dispositionFilename = node.dispositionParameters?.filename?.toLowerCase() ?? '';
// application/octet-stream wuerden ab dann erstmals erkannt), und die ist
// in dieser Aufgabe verboten. Gemeldet im SUMMARY, Entscheidung beim
// Menschen. Die Zusicherung bleibt sichtbar stehen, damit der Befund
// nicht verschwindet.
const dispositionFilename =
((node as any).disposition?.parameters?.filename as string | undefined)?.toLowerCase() ?? '';
// Hier dagegen war die Zusicherung schlicht ueberfluessig: imapflow // Hier dagegen war die Zusicherung schlicht ueberfluessig: imapflow
// deklariert `parameters?: { [key: string]: string }` (imap-flow.d.ts:438). // deklariert `parameters?: { [key: string]: string }` (imap-flow.d.ts:438).
const typeFilename = node.parameters?.name?.toLowerCase() ?? ''; const typeFilename = node.parameters?.name?.toLowerCase() ?? '';
@@ -383,22 +377,21 @@ export class ImapProvider implements InboxProvider {
port: config.port, port: config.port,
// ssl-tls = implicit TLS (port 993); starttls = STARTTLS upgrade (port 143) // ssl-tls = implicit TLS (port 993); starttls = STARTTLS upgrade (port 143)
secure: config.encryption === 'ssl-tls', secure: config.encryption === 'ssl-tls',
requireTLS: config.encryption === 'starttls', // Repariert in 260921-oxm (Befund B-06 aus 260921-m34): hier stand zuvor
// `requireTLS`, eine Option, die imapflow 1.4.3 nirgends kennt und still
// verwirft. Die Bibliothek heisst sie `doSTARTTLS` (imap-flow.d.ts:81).
// true -> vor der Anmeldung auf TLS hochstufen; scheitert, wenn der
// Server kein STARTTLS anbietet (imap-flow.js:1183)
// false -> STARTTLS ausdruecklich aus (imap-flow.js:1210); bei ssl-tls
// ist das noetig, weil secure=true zusammen mit
// doSTARTTLS=true ungueltig waere (imap-flow.js:1201)
doSTARTTLS: config.encryption === 'starttls',
auth: auth:
config.username config.username
? { user: config.username, pass: config.password ?? '' } ? { user: config.username, pass: config.password ?? '' }
: undefined, : undefined,
// T-07-03: suppress imapflow verbose logs — they include auth credentials // T-07-03: suppress imapflow verbose logs — they include auth credentials
logger: false, logger: false,
// BLEIBT als Zusicherung, mit Befund (260921-m34, Aufgabe 3c, D-03): });
// `requireTLS` oben kommt in imapflow 1.4.3 NIRGENDS vor — weder in
// ImapFlowOptions (lib/imap-flow.d.ts) noch im Laufzeitcode
// (lib/imap-flow.js), beides durchsucht. Die Option wird also still
// verworfen; STARTTLS wird nicht durch sie erzwungen. Genau diese
// Zusicherung hat das bisher verdeckt. Sie bleibt trotzdem stehen:
// die Option zu entfernen waere eine stille Reparatur einer falschen
// Annahme (verboten), und der `any`-Befund haelt die Stelle in der
// Zaehlung sichtbar, bis ein Mensch entscheidet. Gemeldet im SUMMARY.
} as any);
} }
} }