// 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")