IMP-02: anhangsverarbeitung-bei-import
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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
03c47d98f4
commit
7238568918
@@ -0,0 +1,132 @@
|
||||
// IMP-02: fehlertolerantes Parsing für den Import-Pfad. Additive
|
||||
// Erweiterung — Parse/parseMultipart (ING-04) bleiben UNVERÄNDERT, deren
|
||||
// Verhalten und Tests sind nicht Gegenstand dieser Kachel. ParseTolerant
|
||||
// nutzt dieselben internen Helfer (readSinglePart, decodeTransferEncoding
|
||||
// usw.), bricht aber bei EINEM fehlerhaften Teil NICHT die gesamte
|
||||
// Nachricht ab (Akzeptanzkriterium 3), sondern verzeichnet den Fehler und
|
||||
// verarbeitet die übrigen Teile weiter. Zusätzlich wird ein
|
||||
// Gesamtgrößenlimit über alle Teile hinweg durchgesetzt
|
||||
// (Akzeptanzkriterium 2 — maxAttachmentSize aus Parse ist nur das Limit
|
||||
// je EINZELNEM Anhang).
|
||||
package mimeparse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/mail"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PartError beschreibt EINEN Teil, der nicht verarbeitet werden konnte —
|
||||
// die übrigen Teile der Nachricht sind davon unberührt.
|
||||
type PartError struct {
|
||||
Filename string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e PartError) Error() string {
|
||||
return fmt.Sprintf("mimeparse: teil %q: %v", e.Filename, e.Err)
|
||||
}
|
||||
|
||||
// ParseTolerant ist wie Parse, bricht aber bei einem fehlerhaften
|
||||
// EINZELNEN Teil (z. B. überdimensionierter Anhang) nicht die gesamte
|
||||
// Nachricht ab — der fehlerhafte Teil landet in den zurückgegebenen
|
||||
// PartErrors, Text und übrige Anhänge werden unangetastet weiter
|
||||
// verarbeitet (Akzeptanzkriterium 3). Nur eine strukturell unlesbare
|
||||
// Nachricht (kaputte Kopfzeilen, fehlende Boundary) liefert weiterhin
|
||||
// einen echten Fehler — davon kann sich kein Teil-für-Teil-Fallback
|
||||
// erholen.
|
||||
func ParseTolerant(r io.Reader, maxAttachmentSize, maxMessageSize int64) (Message, []PartError, error) {
|
||||
msg, err := mail.ReadMessage(r)
|
||||
if err != nil {
|
||||
return Message{}, nil, fmt.Errorf("mimeparse: nachricht lesen: %w", err)
|
||||
}
|
||||
|
||||
mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
body, readErr := readLimited(msg.Body, maxAttachmentSize)
|
||||
if readErr != nil {
|
||||
return Message{}, []PartError{{Filename: "", Err: readErr}}, nil
|
||||
}
|
||||
return Message{Parts: []Part{{ContentType: "text/plain", Content: body, Size: int64(len(body))}}}, nil, nil
|
||||
}
|
||||
|
||||
var result Message
|
||||
var partErrors []PartError
|
||||
budget := maxMessageSize
|
||||
|
||||
if strings.HasPrefix(mediaType, "multipart/") {
|
||||
if err := parseMultipartTolerant(msg.Body, params["boundary"], maxAttachmentSize, &budget, &result, &partErrors); err != nil {
|
||||
return Message{}, partErrors, err
|
||||
}
|
||||
return result, partErrors, nil
|
||||
}
|
||||
|
||||
part, err := readSinglePart(msg.Header.Get("Content-Transfer-Encoding"), mediaType, "", msg.Body, maxAttachmentSize)
|
||||
if err != nil {
|
||||
return Message{}, []PartError{{Filename: "", Err: err}}, nil
|
||||
}
|
||||
result.Parts = append(result.Parts, part)
|
||||
return result, nil, nil
|
||||
}
|
||||
|
||||
func parseMultipartTolerant(r io.Reader, boundary string, maxAttachmentSize int64, budget *int64, result *Message, partErrors *[]PartError) error {
|
||||
if boundary == "" {
|
||||
return errors.New("mimeparse: multipart ohne boundary")
|
||||
}
|
||||
mr := multipart.NewReader(r, boundary)
|
||||
for {
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
// Eine strukturell kaputte Multipart-Hülle (nicht ein
|
||||
// einzelner Teil) kann von hier aus nicht sinnvoll fortgesetzt
|
||||
// werden — kontrollierter Abbruch, wie in Parse.
|
||||
return fmt.Errorf("mimeparse: multipart-teil lesen: %w", err)
|
||||
}
|
||||
|
||||
contentType := p.Header.Get("Content-Type")
|
||||
mediaType, subParams, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
mediaType = "text/plain"
|
||||
}
|
||||
filename := decodeHeaderValue(p.FileName())
|
||||
|
||||
if strings.HasPrefix(mediaType, "multipart/") {
|
||||
if err := parseMultipartTolerant(p, subParams["boundary"], maxAttachmentSize, budget, result, partErrors); err != nil {
|
||||
*partErrors = append(*partErrors, PartError{Filename: filename, Err: err})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if *budget <= 0 {
|
||||
*partErrors = append(*partErrors, PartError{Filename: filename, Err: ErrMessageTooLarge})
|
||||
continue
|
||||
}
|
||||
|
||||
part, err := readSinglePart(p.Header.Get("Content-Transfer-Encoding"), mediaType, filename, p, maxAttachmentSize)
|
||||
if err != nil {
|
||||
// Akzeptanzkriterium 3: NUR dieser eine Teil fällt weg,
|
||||
// Verarbeitung läuft weiter.
|
||||
*partErrors = append(*partErrors, PartError{Filename: filename, Err: err})
|
||||
continue
|
||||
}
|
||||
if part.Size > *budget {
|
||||
*partErrors = append(*partErrors, PartError{Filename: filename, Err: ErrMessageTooLarge})
|
||||
continue
|
||||
}
|
||||
*budget -= part.Size
|
||||
result.Parts = append(result.Parts, part)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrMessageTooLarge wird geliefert (als PartError), wenn die Summe aller
|
||||
// Anhangsgrößen einer Nachricht das Gesamtlimit überschreitet
|
||||
// (Akzeptanzkriterium 2 — je-Nachricht-Limit, zusätzlich zum
|
||||
// je-Anhang-Limit ErrAttachmentTooLarge aus Parse/readLimited).
|
||||
var ErrMessageTooLarge = errors.New("mimeparse: nachricht überschreitet die maximal erlaubte gesamtgröße")
|
||||
Reference in New Issue
Block a user