Anhänge aus importierten Nachrichten extrahieren, validieren und für Weiterverarbeitung bereitstellen. - mimeparse/tolerant.go: additive Erweiterung von ING-04 (Parse/ parseMultipart unverändert) — ParseTolerant bricht bei einem einzelnen fehlerhaften Teil nicht die gesamte Nachricht ab, verzeichnet ihn stattdessen in []PartError. Setzt zusätzlich ein Gesamtgrößenbudget über alle Teile durch (ErrMessageTooLarge), ergänzend zum bereits vorhandenen Je-Anhang-Limit. - attachments/attachments.go: Extract liefert Filename/Size/ DeclaredContentType/VerifiedContentType je Anhang. VerifiedContentType kommt aus http.DetectContentType (echtes Sniffing der Bytes), nicht aus der ungeprüften Absenderbehauptung. Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-02-PRUEFPROTOKOLL.md): 1. TestExtract_OversizedAttachmentIsCorrectlyLimited: überdimensionierter Anhang real übersprungen, Text bleibt real unangetastet. 2. TestExtract_MultipleAttachmentDifferentTypesAllImported: PDF+PNG real beide extrahiert, PNG-Sniffing liefert real image/png. 3. TestExtract_BrokenAttachmentLeavesTextAndOthersUntouched: defektes Base64 in einem Anhang, Text und zweiter Anhang real unangetastet. Kein Umbau: mimeparse Parse/parseMultipart (ING-04) unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
140 lines
5.5 KiB
Go
140 lines
5.5 KiB
Go
package attachments
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestExtract_OversizedAttachmentIsCorrectlyLimited ist die geforderte
|
|
// Pflichtprüfung 1: Nachricht mit überdimensioniertem Anhang wird
|
|
// korrekt begrenzt.
|
|
func TestExtract_OversizedAttachmentIsCorrectlyLimited(t *testing.T) {
|
|
oversized := strings.Repeat("A", 200)
|
|
raw := "From: a@example.com\r\n" +
|
|
"To: b@example.com\r\n" +
|
|
"Subject: Test\r\n" +
|
|
"MIME-Version: 1.0\r\n" +
|
|
"Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
|
|
"Kurzer Nachrichtentext\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: attachment; filename=\"riesig.bin\"\r\n\r\n" +
|
|
oversized + "\r\n" +
|
|
"--b--\r\n"
|
|
|
|
result, err := Extract(strings.NewReader(raw), Options{MaxAttachmentSize: 50, MaxMessageSize: DefaultMaxMessageSize})
|
|
if err != nil {
|
|
t.Fatalf("extract: %v", err)
|
|
}
|
|
if len(result.Attachments) != 0 {
|
|
t.Fatalf("erwartete 0 extrahierte anhänge (überdimensioniert), habe %d", len(result.Attachments))
|
|
}
|
|
if len(result.Skipped) != 1 || result.Skipped[0].Filename != "riesig.bin" {
|
|
t.Fatalf("erwartete genau 1 übersprungenen anhang 'riesig.bin', habe: %+v", result.Skipped)
|
|
}
|
|
if len(result.TextParts) != 1 || string(result.TextParts[0].Content) != "Kurzer Nachrichtentext" {
|
|
t.Fatalf("erwartete unangetasteten text trotz überdimensioniertem anhang, habe: %+v", result.TextParts)
|
|
}
|
|
}
|
|
|
|
// TestExtract_MultipleAttachmentDifferentTypesAllImported ist die
|
|
// geforderte Pflichtprüfung 2: mehrere Anhänge unterschiedlichen Typs
|
|
// werden alle korrekt importiert.
|
|
func TestExtract_MultipleAttachmentDifferentTypesAllImported(t *testing.T) {
|
|
pdfContent := base64.StdEncoding.EncodeToString([]byte("%PDF-1.4 fake pdf bytes"))
|
|
pngContent := base64.StdEncoding.EncodeToString([]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 0, 0, 0})
|
|
|
|
raw := "From: a@example.com\r\n" +
|
|
"To: b@example.com\r\n" +
|
|
"Subject: Test\r\n" +
|
|
"MIME-Version: 1.0\r\n" +
|
|
"Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
|
|
"Anbei zwei Anhänge\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/pdf\r\n" +
|
|
"Content-Disposition: attachment; filename=\"rechnung.pdf\"\r\n" +
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
pdfContent + "\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: image/png\r\n" +
|
|
"Content-Disposition: attachment; filename=\"logo.png\"\r\n" +
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
pngContent + "\r\n" +
|
|
"--b--\r\n"
|
|
|
|
result, err := Extract(strings.NewReader(raw), Options{})
|
|
if err != nil {
|
|
t.Fatalf("extract: %v", err)
|
|
}
|
|
if len(result.Attachments) != 2 {
|
|
t.Fatalf("erwartete 2 extrahierte anhänge, habe %d: %+v", len(result.Attachments), result.Attachments)
|
|
}
|
|
byName := map[string]Attachment{}
|
|
for _, a := range result.Attachments {
|
|
byName[a.Filename] = a
|
|
}
|
|
pdf, ok := byName["rechnung.pdf"]
|
|
if !ok || pdf.DeclaredContentType != "application/pdf" {
|
|
t.Fatalf("pdf-anhang fehlt oder falscher deklarierter typ: %+v", byName)
|
|
}
|
|
if !strings.Contains(pdf.VerifiedContentType, "text/plain") && !strings.Contains(pdf.VerifiedContentType, "application/") {
|
|
// http.DetectContentType erkennt unser Fake-PDF (kein echter PDF-
|
|
// Header) plausibel als Text — hier zählt nur, dass überhaupt ein
|
|
// echter, aus dem Inhalt gesniffter Wert vorliegt (Akzeptanz-
|
|
// kriterium 1: geprüfter statt blind übernommener Content-Type).
|
|
t.Fatalf("erwartete real gesniffeden content-type, habe: %q", pdf.VerifiedContentType)
|
|
}
|
|
png, ok := byName["logo.png"]
|
|
if !ok || png.DeclaredContentType != "image/png" {
|
|
t.Fatalf("png-anhang fehlt oder falscher deklarierter typ: %+v", byName)
|
|
}
|
|
if png.VerifiedContentType != "image/png" {
|
|
t.Fatalf("erwartete real gesniffeten content-type image/png (echte PNG-Magic-Bytes), habe: %q", png.VerifiedContentType)
|
|
}
|
|
}
|
|
|
|
// TestExtract_BrokenAttachmentLeavesTextAndOthersUntouched ist die
|
|
// geforderte Pflichtprüfung 3: ein defekter Anhang lässt Text und übrige
|
|
// Anhänge unangetastet.
|
|
func TestExtract_BrokenAttachmentLeavesTextAndOthersUntouched(t *testing.T) {
|
|
goodContent := base64.StdEncoding.EncodeToString([]byte("echter anhangsinhalt"))
|
|
raw := "From: a@example.com\r\n" +
|
|
"To: b@example.com\r\n" +
|
|
"Subject: Test\r\n" +
|
|
"MIME-Version: 1.0\r\n" +
|
|
"Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: text/plain; charset=utf-8\r\n\r\n" +
|
|
"Wichtiger Nachrichtentext\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: attachment; filename=\"kaputt.bin\"\r\n" +
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
"DAS_IST_KEIN_GUELTIGES_BASE64!!!\r\n" +
|
|
"--b\r\n" +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Disposition: attachment; filename=\"gut.bin\"\r\n" +
|
|
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
|
goodContent + "\r\n" +
|
|
"--b--\r\n"
|
|
|
|
result, err := Extract(strings.NewReader(raw), Options{})
|
|
if err != nil {
|
|
t.Fatalf("extract: %v", err)
|
|
}
|
|
if len(result.TextParts) != 1 || string(result.TextParts[0].Content) != "Wichtiger Nachrichtentext" {
|
|
t.Fatalf("erwartete unangetasteten text trotz defektem anhang, habe: %+v", result.TextParts)
|
|
}
|
|
if len(result.Attachments) != 1 || result.Attachments[0].Filename != "gut.bin" {
|
|
t.Fatalf("erwartete den guten anhang unangetastet, habe: %+v", result.Attachments)
|
|
}
|
|
if string(result.Attachments[0].Content) != "echter anhangsinhalt" {
|
|
t.Fatalf("guter anhang hat unerwarteten inhalt: %q", result.Attachments[0].Content)
|
|
}
|
|
}
|