enmime

repository·main·Indexed 19 days ago

https://github.com/jhillyerd/enmime

A Go-based MIME encoding and decoding library focused on the generation and parsing of MIME-encoded emails. It provides a fluent MailBuilder interface for constructing complex MIME messages, an Envelope type for high-level access to email content, and utilities like mime-dump for debugging and mime-extractor for extracting attachments.

Tokens
10.9K
Snippets
43
Records
51
Agent score
67%

What's inside enmime

  1. Overview of enmime

    main
    enmime is a Go library designed for MIME encoding and decoding, specifically optimized for generating and parsing MIME-encoded emails. It is developed alongside the Inbucket email service and provides a fluent interface builder for constructing complex MIME messages.
  2. Understand the mime-dump output format

    main

    When running mime-dump, the output is a Markdown document organized into the following sections:

    • Envelope: Contains headers like From, To, and Subject.
    • Body Text: The plain text content of the email.
    • Body HTML: The HTML content of the email.
    • Attachment List: A list of files attached to the email.
    • MIME Part Tree: A visual representation of the MIME hierarchy (e.g., multipart/alternative, text/plain, image/png) including dispositions and filenames.
    Envelope
    --------
    From: James Hillyerd <james@makita.skynet>  
    To: greg@nobody.com  
    Subject: MIME test 1  
    
    Body Text
    ---------
    Test of text section
    
    Body HTML
    ---------
    Test of HTML section
    
    Attachment List
    ---------------
    
    MIME Part Tree
    --------------
        multipart/alternative
        |-- text/plain
        `-- multipart/related
            |-- text/html
            `-- image/png, disposition: inline, filename: "favicon.png"
  3. Use the mime-dump utility for debugging

    main

    The mime-dump utility is used to debug enmime parsing by converting an email file into a human-readable Markdown document. This document describes the email's envelope, body text, body HTML, attachments, and the MIME part tree structure.

    # Build the utility
    go build
    
    # Run it against an email file
    ./mime-dump ../test-data/mail/html-mime-inline.raw
  4. Configure error recovery with ReadPartErrorPolicy

    main

    The ReadPartErrorPolicy type allows you to define how the parser should behave when an error occurs while reading a Part's content. The policy function receives the *Part and the error, returning a bool that indicates whether the parser should attempt to recover (e.g., by using partial content).

    // Example of a policy that recovers from corrupt base64 in text parts
    policy := enmime.AllowCorruptTextPartErrorPolicy
    
    // Apply it during parser creation
    parser := enmime.NewParser(enmime.WithReadPartErrorPolicy(policy))
  5. How MailBuilder constructs the MIME tree

    main

    When Build() is called, MailBuilder automatically determines the most efficient MIME structure based on the provided content. It builds a tree of Part structs following this hierarchy:

    1. multipart/mixed (Root)
      • multipart/related
        • multipart/alternative
          • text/plain
          • text/html
        • Other parts (e.g., images with Content-ID)
        • Inlines
      • Attachments

    If only text/plain or text/html is provided, the tree is simplified. If both are provided, they are wrapped in a multipart/alternative container.

  6. Use the Sender interface to send emails

    main

    The Sender interface defines the contract for sending MIME messages. Implementing this interface allows you to swap out different delivery mechanisms (like SMTP, SendGrid, or a mock sender for testing) while keeping your message construction logic decoupled from the transport layer.

    To send a message, call Send with:

    • reversePath: The email address used for delivery error reporting (the MAIL FROM command in SMTP).
    • recipients: A slice of strings containing the destination email addresses.
    • msg: The raw byte slice of the MIME message (including headers like From, To, and Subject).

    Note on BCC: To send a BCC message, include the recipient's address in the recipients slice but omit it from the msg headers.

    // Example of the Sender interface signature
    type Sender interface {
    	Send(reversePath string, recipients []string, msg []byte) error
    }
  7. How MIME encoding is determined for a Part

    main

    When calling Encode, the library automatically selects the Content-Transfer-Encoding (CTE) using the following logic:

    1. Message Types: If the ContentType starts with message/, it uses 8bit (per RFC 1341).
    2. Text Content: For non-message types that are identified as text, it scans the content. If the content contains mostly ASCII, it uses quoted-printable. If the density of non-ASCII characters exceeds a threshold (20%), it switches to base64.
    3. Binary/Other: Defaults to base64 for non-text content.
    4. Raw: If the part was parsed with rawContent enabled, it uses teRaw (no encoding).

    This logic ensures that the resulting MIME message is as efficient as possible while remaining compliant with RFC standards.

  8. Use MailBuilder to construct MIME messages

    main

    The MailBuilder provides a fluent, immutable-style interface for constructing complex MIME messages. Each manipulation method returns a copy of the MailBuilder, allowing you to chain calls. This design makes the builder thread-safe for reuse if the underlying data is not modified externally.

    To use it, start with enmime.Builder(), chain your configuration methods (like From, To, Subject, Text, HTML, etc.), and finally call Build() to generate a *Part tree or Send() to transmit the message.

    package main
    
    import (
    	"github.com/jhillyerd/enmime/v2"
    )
    
    func main() {
    	builder := enmime.Builder().
    		From("Sender Name", "sender@example.com").
    		To("Recipient", "recipient@example.com").
    		Subject("Hello World").
    		Text([]byte("This is the plain text body")).
    		HTML([]byte("<h1>This is the HTML body</h1>"))
    
    	part, err := builder.Build()
    	if err != nil {
    		panic(err)
    	}
    	// Use the resulting *Part tree...
    }
  9. Parse an email message into an Envelope

    main

    To parse a MIME email message, use ReadEnvelope(r io.Reader). This function reads the content from the provided reader and returns an *Envelope.

    An Envelope is a simplified wrapper that automatically:

    • Downconverts HTML to plain text if no text/plain part is present (unless configured otherwise).
    • Sorts parts into Attachments, Inlines, and OtherParts based on their Content-Disposition.
    • Collects parsing errors from all nested parts into the Errors slice.

    If you need to use specific parser configurations, use the Parser.ReadEnvelope(r io.Reader) method instead of the package-level ReadEnvelope.

    import "github.com/jhillyerd/enmime/v2"
    
    // ...
    // r is an io.Reader containing the raw MIME message
    envelope, err := enmime.ReadEnvelope(r)
    if err != nil {
    	// handle error
    }
    
    // Access content
    fmt.Println(envelope.Text)
    fmt.Println(envelope.HTML)
    for _, attachment := range envelope.Attachments {
    	fmt.Println("Attachment found:", attachment.Header.Get("Content-Disposition"))
    }
  10. Handle errors in MailBuilder

    main

    The MailBuilder captures errors during the construction process (specifically during file I/O in AddFileAttachment, AddFileInline, or AddFileOtherPart).

    If an error occurs during a file operation, subsequent builder calls will be ignored, and the error will be stored. You must check for this error using the Error() method before calling Build() or Send().

    builder := enmime.Builder().AddFileAttachment("missing.txt")
    if err := builder.Error(); err != nil {
    	// Handle the error
    }
  11. Use AllowCorruptTextPartErrorPolicy to recover text parts

    main

    The AllowCorruptTextPartErrorPolicy is a built-in error policy designed to recover partial content when encountering a base64.CorruptInputError, specifically when the part's ContentType is text/plain or text/html. This is useful for handling slightly malformed text-based MIME parts without failing the entire parsing process.

    // Use this policy to allow the parser to continue even if text parts have base64 corruption
    parser := enmime.NewParser(enmime.WithReadPartErrorPolicy(enmime.AllowCorruptTextPartErrorPolicy))
  12. Determine if a part contains text content

    main
    The TextContent() bool method returns true if the part's ContentType indicates it is text-based (e.g., starts with text/ or is a multipart/ type). This is used to determine appropriate content transfer encoding schemes. If ContentType is empty, it is treated as text/plain; charset=us-ascii per RFC 2045.