Skip to content

feat: HTML-Text in mail_create_draft #63 - #65

Merged
mahype merged 17 commits into
mainfrom
feature/63-html-mail-entwuerfe
Nov 3, 2026
Merged

mahype merged 17 commits into
mainfrom
feature/63-html-mail-entwuerfe

Conversation

@mahype

@mahype mahype commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • mail_create_draft nimmt ein optionales html_body an. Damit wird die Mail multipart/alternative mit Text- und HTML-Teil, bei Anhängen eingebettet in multipart/mixed. Beide Teile sind quoted-printable, lange HTML-Zeilen bleiben also unter dem Zeilenlimit von RFC 5322.
  • Die Textfassung leitet TorroMail selbst aus dem HTML ab, wie Mail-Programme es beim Versand von HTML tun: Tags raus, Link-Ziele als <url>, Bilder als [image: alt <src>]. Genau dieser Text steht im Entwurf und in der Freigabe. Abgelehnt werden ein zusätzlicher body, leeres HTML, HTML ohne sichtbare Wörter und html_body über 20 MiB.
  • Die Textfassung ist bewusst eine einfache Annäherung, kein Browser-Nachbau, und läuft linear in der Eingabe. Die Leseansicht (html_to_text) und Mails ohne html_body bleiben Byte für Byte gleich, ein Test hält das fest.

Erledigt-Abgleich

  • mail_create_draft nimmt html_body an, das Schema beschreibt das Feld. — automatisch: cargo test -p torromail-mcp --test tool_contract (the_draft_schema_offers_html_body_and_no_longer_requires_body, an_html_draft_is_created)
  • Ein Entwurf mit HTML ist multipart/alternative mit Text- und HTML-Teil, auch zusammen mit Anhängen. — automatisch: cargo test -p torromail-core (compose_with_html_builds_a_quoted_printable_alternative, compose_with_html_and_attachment_nests_the_alternative_in_mixed), send_journal::an_html_local_draft_keeps_its_html_and_sends_it_unchanged
  • Lange HTML-Zeilen erzeugen gültige MIME-Daten. — automatisch: compose_with_a_long_html_line_keeps_every_line_short, quoted_printable_wraps_long_lines_without_splitting_escapes, send_journal::an_html_local_draft_… (1500-Zeichen-Zeile)
  • Ohne html_body bleibt die Nachricht Byte für Byte wie heute, ebenso Idempotenz und Fingerprint bestehender Entwürfe. — automatisch: core_contract::compose_without_html_is_byte_identical (Literal vom unveränderten Code), dazu alle bisherigen Draft- und Journal-Tests grün
  • Lokale Entwürfe und das Send-Journal behalten den HTML-Teil, der Versand verschickt ihn unverändert. — automatisch: send_journal::an_html_local_draft_keeps_its_html_and_sends_it_unchanged (gespeichertes raw = übergebenes MIME = SMTP-DATA)
  • Die Freigabe-Vorschau in App und MCP zeigt lesbaren Text. — automatisch (MCP, --send-action list): send_journal::an_html_local_draft_…; App: von Hand (Test plan)
  • Tests für Core-Compose, Tool-Vertrag und Dovecot-Integration. — automatisch: cargo test und scripts/test-drafts-dovecot.sh (alle drei Namespaces grün, inkl. real_dovecot_html_draft_with_attachment_is_verified_read_and_sent)
  • AGENTS.md und die Tool-Doku nennen html_body. — automatisch: im Diff nachgesehen (AGENTS.md, docs/architecture.md, Schema-Beschreibung)

Test plan

  • Einen Assistenten eine HTML-Mail mit Link an die eigene Adresse vorbereiten lassen und in der App „Review and approve“ öffnen: lesbarer Text ohne Tags, hinter dem Link steht das Ziel.
  • Die freigegebene Mail in Apple Mail öffnen: Sie ist formatiert, und die reine Textalternative zeigt denselben Text wie die Freigabe.
  • swift build --package-path apps/TorroMailApp --scratch-path apps/TorroMailApp/.build mit Xcode-Toolchain: Lief hier nicht, weil die Command Line Tools keine SwiftUI-Makros haben und die Xcode-Lizenz nicht akzeptiert ist. Swift-Code ist nicht geändert.

Bewusst offen

  • Die Textfassung ist eine Annäherung: Abweichungen vom Browser-Parsing (Kommentarformen wie <!-->, relative Ziele unter <base>, <image>, CDATA), CSS-Text, Formularwerte, Bildinhalt und Outlook-Bedingungskommentare können beim Empfänger mehr zeigen als die Freigabe. Laut Abgrenzung wird das HTML nicht bereinigt, und der Nutzer hat nach drei Review-Runden entschieden, diese Fälle nicht weiter nachzubauen; docs/architecture.md nennt die Grenze.
  • Abweichung vom Plan: Auch ein nicht geschlossener oder durch ein neues <a> beendeter Link nennt sein Ziel (Plan: „ungeschlossenes verfällt“).
  • swift build lief in dieser Umgebung nicht (Command Line Tools ohne SwiftUI-Makros, Xcode-Lizenz nicht akzeptiert); Swift-Code ist nicht geändert.

Umgesetzt mit: claude-opus-5-5 (stark), Effort high

Closes #63

🤖 Generated with Claude Code

mahype and others added 13 commits November 3, 2026 07:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
encode_quoted_printable writes RFC 2045 bodies with soft breaks at 76
columns; html_to_plain_text derives the plain-text alternative and names
link targets. The reading view (html_to_text) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With html_body the draft becomes multipart/alternative; TorroMail derives
the plain-text part (with link targets) and stores it as the approved
text. A separate body, blank HTML or HTML without readable text is
refused. body is no longer required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An <a> without </a>, or one ended by the next <a>, now still names its
target; a > inside a quoted attribute no longer leaks markup into the
derived plain text. The reading view is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quotes open a value only after =, a < that cannot start a tag is text,
only whole script/style/title elements are dropped, and <base>, <area>
and xlink:href links name their targets. Stray quotes or a bare < could
otherwise hide text the recipient sees from the approval. The reading
view is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#63

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mahype mahype added the awaiting-review PR wartet auf Review label Nov 3, 2026
@mahype mahype self-assigned this Nov 3, 2026
@mahype mahype added in-review Review läuft and removed awaiting-review PR wartet auf Review labels Nov 3, 2026
@mahype

mahype commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner Author

Code-Review

Fazit: Überarbeitung empfohlen

Die abgeleitete Textfassung lässt sich mit gültigen HTML-Kommentarformen umgehen: Der Empfänger sieht dann Text, der in der Freigabe fehlt. Genau das soll Entscheidung 1 des Plans verhindern.

Kritische Probleme

  • crates/torromail-core/src/mime.rs:546 — html_to_plain_text entfernt Kommentare mit remove_ranges(html, "<!--", "-->"), Browser beenden einen Kommentar aber schon bei <!-->, <!---> und --!>. Damit wird aus <p>Hallo</p><!-->Zahle an DE99 evil<!-- x --> die Freigabe-Vorschau „Hallo“, während Apple Mail den Satz anzeigt (im Scratch-Test nachvollzogen, alle drei Formen). Kommentare wie im HTML-Parser beenden und je Form einen Test in mime.rs ergänzen. → verworfen: Der Nutzer hat entschieden, dass die Textfassung eine einfache Annäherung bleibt wie in Mail-Programmen; Abweichungen vom Browser-Parsing wie Kommentarformen stehen in docs/architecture.md als Grenze.

Anmerkungen

  • crates/torromail-core/src/mime.rs:551 — Mit <base href> zeigt die Textfassung nur das relative Ziel (<a href="login">Ihre Bank</a> wird „Ihre Bank “, #pay wird „<#pay>“), die Domain aus <base> sieht der Freigebende nirgends. Relative Ziele gegen <base> auflösen oder das Basis-Ziel einmal nennen. → verworfen: Der Nutzer hat entschieden, dass die Textfassung eine einfache Annäherung bleibt; die -Auflösung verursachte eine Ausgabe-Vervielfachung und ist als Grenze dokumentiert.
  • crates/torromail-core/src/mime.rs:566 — find_element wandelt bei jedem Aufruf das ganze Dokument in Kleinbuchstaben, remove_elements ruft es je Element einmal auf; das ist quadratisch (8000 leere <style>-Elemente, 128 KB: 40 ms, Faktor 5 je Verdopplung) und läuft vor der 20-MiB-Prüfung in compose_and_append, ein großes HTML legt den Server also für Minuten lahm. Den Haystack einmal je Durchlauf erzeugen und html_body vor der Ableitung auf MAX_MESSAGE_BYTES begrenzen. → Nacharbeit im PR (erledigt in bd271f2)
  • crates/torromail-mcp/src/lib.rs:3077 — Auch nach den Korrekturen zeigt der Empfänger mehr, als freigegeben wird: Bilder (z.B. ein Bild mit Bankverbindung), per CSS content: erzeugter Text und <input value> fehlen in der Textfassung, und die Doku nennt als Grenze nur Outlook-Bedingungskommentare. Zu entscheiden ist, ob die Textfassung Bilder etwa als [Bild: alt <src>] nennt bzw. die Freigabe in der App „enthält HTML mit N Bildern“ anzeigt, oder ob es bei „best effort“ bleibt und docs/architecture.md diese Grenzen ausdrücklich nennt. → Entscheidung bei dir (entschieden: umsetzen, Der Nutzer hat entschieden, Bilder als [image: alt ] in der Textfassung zu nennen; CSS-Text, Formularwerte und Bildinhalt sind als Grenze dokumentiert.; erledigt in bd271f2)

Kein Prüfbedarf

Kein Handlungsbedarf, diese Teile kannst du überfliegen:

  • compose_message_with_attachments als None-Zweig der neuen Funktion samt ausgelagertem append_attachment_parts: durch den Byte-Literal-Test compose_without_html_is_byte_identical festgehalten.
  • encode_quoted_printable und die festen Boundaries: Zeilenlänge, Escapes und Rundreise sind getestet, =_ kann in QP- und Base64-Daten nicht vorkommen.
  • Schema-Änderung, draft_bodies samt Fehlermeldungen und die Tests in tool_contract.rs, send_journal.rs und drafts_dovecot.rs: decken den Erledigt-Abgleich ab, cargo test ist grün.
  • Doku in AGENTS.md und docs/architecture.md: passt zum Code und zur Produktgrenze.

@mahype mahype added changes-requested Review verlangt Überarbeitung and removed in-review Review läuft labels Nov 3, 2026
mahype and others added 4 commits November 3, 2026 08:49
Comments end at <!-->, <!---> and --!> as in the HTML tokenizer, so a
comment can no longer hide visible text from the approval. Relative link
and image targets are resolved against <base href>. Images appear as
[image: alt <src>]. Raw-text elements are found on one lowercased copy,
which removes the quadratic cost on large documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops <base> resolution and browser-style comment ending again; both
were chasing browser fidelity the issue rules out (no sanitizing, the
person approves the send) and added a quadratic comment scan and an
output blow-up under <base>. Link targets and image markers stay; the
derivation is linear in the input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mahype mahype added in-review Review läuft and removed changes-requested Review verlangt Überarbeitung labels Nov 3, 2026
@mahype

mahype commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Code-Review

Fazit: Keine Blocker

Der PR erfüllt Plan und Erledigt-Abgleich, die offenen Punkte des letzten Reviews sind behoben oder laut Nutzerentscheidung bewusst offen, und die abgeleitete Textfassung läuft jetzt linear in der Eingabe.

Kritische Probleme

Keine.

Erledigt seit dem letzten Review

  • crates/torromail-core/src/mime.rs:591 — Quadratische Ableitung: remove_elements und remove_ranges erzeugen den Haystack einmal je Durchlauf, html_body über 20 MiB wird in draft_bodies vor dem Parsen abgelehnt und ist in tool_contract.rs getestet. (erledigt in bd271f2, 9f8f706; nach ffd10dd weiterhin linear)
  • crates/torromail-core/src/mime.rs:719 — Bilder erscheinen als [image: alt <src>] und zählen nicht als lesbare Wörter; CSS-Text, Formularwerte und Bildinhalt nennt docs/architecture.md als Grenze. (erledigt in bd271f2, e94d1a6)

Hinweise

  • Kommentarformen wie <!--> und relative Ziele unter <base> bleiben laut Nutzerentscheidung offen (PR-Body, „Bewusst offen“); docs/architecture.md nennt beide als Grenze, nicht erneut bewertet.
  • Abweichung vom Plan: Ein nicht geschlossener oder durch ein neues <a> beendeter Link nennt sein Ziel; das zeigt dem Freigebenden mehr, nicht weniger, und ist im PR-Body offengelegt.
  • Von Hand offen (Test plan): Freigabe-Vorschau in der App, Darstellung in Apple Mail und swift build mit Xcode-Toolchain; Swift-Code ist nicht geändert, swift build scheitert hier mit den Command Line Tools an den SwiftUI-Makros.

Kein Prüfbedarf

Kein Handlungsbedarf, diese Teile kannst du überfliegen:

  • compose_message_with_attachments als None-Zweig von compose_message_with_alternatives und html_to_text über render_html(…, None): verhaltensgleich, durch compose_without_html_is_byte_identical und the_reading_view_still_hides_link_targets festgehalten.
  • encode_quoted_printable und die festen Boundaries: Zeilen höchstens 76 Zeichen, kein Leerraum vor CRLF, =_ kommt in QP- und Base64-Daten nicht vor; SMTP-Dot-Stuffing übernimmt smtp.rs.
  • Fingerprint und Idempotenz: sha256 über das fertige MIME, der HTML-Teil ist also enthalten.
  • Tests in tool_contract.rs, send_journal.rs, drafts_dovecot.rs und core_contract.rs: decken den Erledigt-Abgleich ab, cargo test und cargo build -p torromail-mcp sind grün.
  • Doku in AGENTS.md und docs/architecture.md: passt zum Code nach ffd10dd und zur Produktgrenze.

@mahype mahype added merge-ready Review ohne Blocker, wartet auf Merge-Entscheidung and removed in-review Review läuft labels Nov 3, 2026
@mahype
mahype marked this pull request as ready for review November 3, 2026 10:22
@mahype
mahype merged commit 6ea7a7c into main Nov 3, 2026
4 checks passed
@mahype mahype mentioned this pull request Nov 4, 2026
2 of 3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-ready Review ohne Blocker, wartet auf Merge-Entscheidung

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mail-Versand: HTML-Text in mail_create_draft unterstützen

1 participant