// 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 }