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
133 lines
4.8 KiB
Go
133 lines
4.8 KiB
Go
// 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")
|