Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,23 @@ func (p Parser) ReadEnvelope(r io.Reader) (*Envelope, error) {
if err != nil {
return nil, errors.WithMessage(err, "Failed to ReadParts")
}
return p.EnvelopeFromPart(root)

e, err := p.EnvelopeFromPart(root)
if err != nil {
return nil, errors.WithMessage(err, "Failed to EnvelopeFromPart")
}

err = e.GatherNestedErrors()
if err != nil {
return nil, errors.WithMessage(err, "Failed to GatherNestedErrors")
}

return e, nil
}

// EnvelopeFromPart uses the provided Part tree to build an Envelope, downconverting HTML to plain
// text if needed, and sorting the attachments, inlines and other parts into their respective
// slices. Errors are collected from all Parts and placed into the Envelopes Errors slice.
// slices. Gather errors from all parts with Envelope.GatherNestedErrors().
func EnvelopeFromPart(root *Part) (*Envelope, error) {
return defaultParser.EnvelopeFromPart(root)
}
Expand Down Expand Up @@ -210,7 +221,12 @@ func (p Parser) EnvelopeFromPart(root *Part) (*Envelope, error) {
}
}

// Copy part errors into Envelope.
return e, nil
}

// GatherNestedErrors gathers errors from the entire part tree (including the root) and adds them to the Envelope.
func (e *Envelope) GatherNestedErrors() error {
// Copy part errors from all nested/child/sibling parts into Envelope.
if e.Root != nil {
_ = e.Root.DepthMatchAll(func(part *Part) bool {
// Using DepthMatchAll to traverse all parts, don't care about result.
Expand All @@ -219,7 +235,7 @@ func (p Parser) EnvelopeFromPart(root *Part) (*Envelope, error) {
})
}

return e, nil
return nil
}

// parseTextOnlyBody parses a plain text message in root that has MIME-like headers, but
Expand Down
52 changes: 52 additions & 0 deletions envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func TestParseNonMime(t *testing.T) {

func TestParseNonMimeHTML(t *testing.T) {
msg := test.OpenTestData("mail", "non-mime-html.raw")

e, err := enmime.ReadEnvelope(msg)

if err != nil {
Expand All @@ -87,6 +88,57 @@ func TestParseNonMimeHTML(t *testing.T) {
}
}

// TestEnvelopeFromPartDoesNotGatherErrors verifies that EnvelopeFromPart does not collect errors,
// while GatherNestedErrors does. This enforces the separation of concerns: EnvelopeFromPart builds
// the envelope structure, and GatherNestedErrors is responsible for error collection.
func TestEnvelopeFromPartDoesNotGatherErrors(t *testing.T) {
// Use a message that generates errors (HTML without plain text)
msg := test.OpenTestData("mail", "non-mime-html.raw")
parser := enmime.NewParser()
root, err := parser.ReadParts(msg)
if err != nil {
t.Fatalf("Failed to read parts: %v", err)
}

// Call EnvelopeFromPart directly (not ReadEnvelope)
e, err := parser.EnvelopeFromPart(root)
if err != nil {
t.Fatalf("Failed to create envelope from part: %v", err)
}

// EnvelopeFromPart should NOT have collected errors
if len(e.Errors) != 0 {
t.Errorf("EnvelopeFromPart should not gather errors, but got %d errors:", len(e.Errors))
for _, err := range e.Errors {
t.Logf(" - %v", err)
}
}

// Verify that the envelope structure was built correctly
if e.HTML == "" {
t.Error("Expected HTML content from envelope structure building")
}

// Now call GatherNestedErrors
err = e.GatherNestedErrors()
if err != nil {
t.Fatalf("GatherNestedErrors failed: %v", err)
}

// GatherNestedErrors SHOULD have collected the error
if len(e.Errors) != 1 {
t.Errorf("GatherNestedErrors should collect exactly 1 error, got %d", len(e.Errors))
} else if e.Errors[0].Name != enmime.ErrorPlainTextFromHTML {
t.Errorf("Expected error %q, got %q", enmime.ErrorPlainTextFromHTML, e.Errors[0].Name)
}

// Verify text was downconverted from HTML
want := "This is *a* *test* mailing"
if !strings.Contains(e.Text, want) {
t.Errorf("Expected %q to contain %q", e.Text, want)
}
}

func TestParseMimeTree(t *testing.T) {
msg := test.OpenTestData("mail", "attachment.raw")
e, err := enmime.ReadEnvelope(msg)
Expand Down
Loading