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
110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
// Package attachments implementiert IMP-02: Anhänge aus importierten
|
|
// Nachrichten extrahieren, validieren und für die Weiterverarbeitung
|
|
// (Speicherung, Virenscan — beides spätere Kacheln, siehe "Nicht
|
|
// Bestandteil dieser Kachel") bereitstellen. Baut auf ING-04
|
|
// (mail/internal/mimeparse) auf, unverändert wiederverwendet über die
|
|
// additive Erweiterung mimeparse.ParseTolerant — kein Umbau der
|
|
// bestehenden, fertigen ING-04-Logik.
|
|
package attachments
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mimeparse"
|
|
)
|
|
|
|
// Attachment ist EIN extrahierter, validierter Anhang
|
|
// (Akzeptanzkriterium 1: Originaldateiname, Größe, geprüfter
|
|
// Content-Type).
|
|
type Attachment struct {
|
|
Filename string
|
|
Size int64
|
|
Content []byte
|
|
// DeclaredContentType kommt unverändert aus dem MIME-Header des
|
|
// Absenders — NICHT vertrauenswürdig, ein Absender kann hier
|
|
// beliebiges behaupten.
|
|
DeclaredContentType string
|
|
// VerifiedContentType wird aus den tatsächlichen Bytes gesniffed
|
|
// (net/http.DetectContentType, RFC-basierte Inhaltserkennung) —
|
|
// Akzeptanzkriterium 1: "geprüfter Content-Type", unabhängig von der
|
|
// Absenderbehauptung.
|
|
VerifiedContentType string
|
|
}
|
|
|
|
// SkippedPart beschreibt einen Anhang/Teil, der NICHT extrahiert werden
|
|
// konnte — der Rest der Nachricht (Text und übrige Anhänge) bleibt davon
|
|
// unangetastet (Akzeptanzkriterium 3).
|
|
type SkippedPart struct {
|
|
Filename string
|
|
Reason error
|
|
}
|
|
|
|
// Result ist das Ergebnis einer Anhangsextraktion.
|
|
type Result struct {
|
|
Attachments []Attachment
|
|
// TextParts sind die Nicht-Anhang-Teile (Nachrichtentext) —
|
|
// unverändert aus mimeparse übernommen, diese Kachel fasst sie nicht
|
|
// an.
|
|
TextParts []mimeparse.Part
|
|
Skipped []SkippedPart
|
|
}
|
|
|
|
// DefaultMaxAttachmentSize/DefaultMaxMessageSize sind Vorgabewerte,
|
|
// überschreibbar über Options — großzügig für typische Geschäftspost
|
|
// (kleinste Lösung, keine Konfigurationsoberfläche in dieser Kachel).
|
|
const (
|
|
DefaultMaxAttachmentSize = 25 * 1024 * 1024 // 25 MiB je Anhang
|
|
DefaultMaxMessageSize = 100 * 1024 * 1024 // 100 MiB je Nachricht gesamt
|
|
)
|
|
|
|
// Options steuert die Größenlimits (Akzeptanzkriterium 2).
|
|
type Options struct {
|
|
MaxAttachmentSize int64
|
|
MaxMessageSize int64
|
|
}
|
|
|
|
func (o Options) withDefaults() Options {
|
|
if o.MaxAttachmentSize <= 0 {
|
|
o.MaxAttachmentSize = DefaultMaxAttachmentSize
|
|
}
|
|
if o.MaxMessageSize <= 0 {
|
|
o.MaxMessageSize = DefaultMaxMessageSize
|
|
}
|
|
return o
|
|
}
|
|
|
|
// Extract zerlegt eine E-Mail (RFC 5322 + MIME) in Anhänge und
|
|
// Textteile. Ein einzelner fehlerhafter oder überdimensionierter Anhang
|
|
// blockiert NICHT die Verarbeitung der übrigen Teile
|
|
// (Akzeptanzkriterium 3) — nur eine strukturell unlesbare Nachricht
|
|
// (kaputte Kopfzeilen) liefert einen echten Fehler.
|
|
func Extract(r io.Reader, opts Options) (Result, error) {
|
|
opts = opts.withDefaults()
|
|
|
|
msg, partErrors, err := mimeparse.ParseTolerant(r, opts.MaxAttachmentSize, opts.MaxMessageSize)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
var result Result
|
|
for _, pe := range partErrors {
|
|
result.Skipped = append(result.Skipped, SkippedPart{Filename: pe.Filename, Reason: pe.Err})
|
|
}
|
|
|
|
for _, part := range msg.Parts {
|
|
if !part.IsAttachment {
|
|
result.TextParts = append(result.TextParts, part)
|
|
continue
|
|
}
|
|
result.Attachments = append(result.Attachments, Attachment{
|
|
Filename: part.Filename,
|
|
Size: part.Size,
|
|
Content: part.Content,
|
|
DeclaredContentType: part.ContentType,
|
|
VerifiedContentType: http.DetectContentType(part.Content),
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|