enmime

package module
v1.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2020 License: MIT Imports: 26 Imported by: 0

README

enmime

PkgGoDev Build Status Go Report Card Coverage Status

enmime is a MIME encoding and decoding library for Go, focused on generating and parsing MIME encoded emails. It is being developed in tandem with the Inbucket email service.

enmime includes a fluent interface builder for generating MIME encoded messages, see the wiki for example Builder Usage.

See our Pkg Docs for examples and API usage information.

Development Status

enmime is near production quality: it works but struggles to parse a small percentage of emails. It's possible the API will evolve slightly before the 1.0 release.

See CONTRIBUTING.md for more information.

About

enmime is written in Go.

enmime is open source software released under the MIT License. The latest version can be found at https://github.com/xhbase/enmime

Documentation

Overview

Package enmime implements a MIME encoding and decoding library. It's built on top of Go's included mime/multipart support where possible, but is geared towards parsing MIME encoded emails.

Overview

The enmime API has two conceptual layers. The lower layer is a tree of Part structs, representing each component of a decoded MIME message. The upper layer, called an Envelope provides an intuitive way to interact with a MIME message.

Part Tree

Calling ReadParts causes enmime to parse the body of a MIME message into a tree of Part objects, each of which is aware of its content type, filename and headers. The content of a Part is available as a slice of bytes via the Content field.

If the part was encoded in quoted-printable or base64, it is decoded prior to being placed in Content. If the Part contains text in a character set other than utf-8, enmime will attempt to convert it to utf-8.

To locate a particular Part, pass a custom PartMatcher function into the BreadthMatchFirst() or DepthMatchFirst() methods to search the Part tree. BreadthMatchAll() and DepthMatchAll() will collect all Parts matching your criteria.

Envelope

ReadEnvelope returns an Envelope struct. Behind the scenes a Part tree is constructed, and then sorted into the correct fields of the Envelope.

The Envelope contains both the plain text and HTML portions of the email. If there was no plain text Part available, the HTML Part will be down-converted using the html2text library1. The root of the Part tree, as well as slices of the inline and attachment Parts are also available.

Headers

Every MIME Part has its own headers, accessible via the Part.Header field. The raw headers for an Envelope are available in Root.Header. Envelope also provides helper methods to fetch headers: GetHeader(key) will return the RFC 2047 decoded value of the specified header. AddressList(key) will convert the specified address header into a slice of net/mail.Address values.

Errors

enmime attempts to be tolerant of poorly encoded MIME messages. In situations where parsing is not possible, the ReadEnvelope and ReadParts functions will return a hard error. If enmime is able to continue parsing the message, it will add an entry to the Errors slice on the relevant Part. After parsing is complete, all Part errors will be appended to the Envelope Errors slice. The Error* constants can be used to identify a specific class of error.

Please note that enmime parses messages into memory, so it is not likely to perform well with multi-gigabyte attachments.

enmime is open source software released under the MIT License. The latest version can be found at https://github.com/xhbase/enmime

Index

Examples

Constants

View Source
const (
	// ErrorMalformedBase64 name.
	ErrorMalformedBase64 = "Malformed Base64"
	// ErrorMalformedHeader name.
	ErrorMalformedHeader = "Malformed Header"
	// ErrorMissingBoundary name.
	ErrorMissingBoundary = "Missing Boundary"
	// ErrorMissingContentType name.
	ErrorMissingContentType = "Missing Content-Type"
	// ErrorCharsetConversion name.
	ErrorCharsetConversion = "Character Set Conversion"
	// ErrorContentEncoding name.
	ErrorContentEncoding = "Content Encoding"
	// ErrorPlainTextFromHTML name.
	ErrorPlainTextFromHTML = "Plain Text from HTML"
	// ErrorCharsetDeclaration name.
	ErrorCharsetDeclaration = "Character Set Declaration Mismatch"
)

Variables

View Source
var AddressHeaders = map[string]bool{
	"bcc":             true,
	"cc":              true,
	"delivered-to":    true,
	"from":            true,
	"reply-to":        true,
	"to":              true,
	"sender":          true,
	"resent-bcc":      true,
	"resent-cc":       true,
	"resent-from":     true,
	"resent-reply-to": true,
	"resent-to":       true,
	"resent-sender":   true,
}

AddressHeaders is the set of SMTP headers that contain email addresses, used by Envelope.AddressList(). Key characters must be all lowercase.

Functions

func DecodeHeaders added in v1.0.3

func DecodeHeaders(b []byte, addtlHeaders ...string) (textproto.MIMEHeader, error)

DecodeHeaders returns a limited selection of mime headers for use by user agents Default header list:

"Date", "Subject", "Sender", "From", "To", "CC" and "BCC"

Additional headers provided will be formatted canonically:

h, err := enmime.DecodeHeaders(b, "content-type", "user-agent")

func ParseMediaType added in v1.0.3

func ParseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error)

ParseMediaType is a more tolerant implementation of Go's mime.ParseMediaType function.

Tolerances accounted for:

  • Missing ';' between content-type and media parameters
  • Repeating media parameters
  • Unquoted values in media parameters containing 'tspecials' characters

Types

type Envelope

type Envelope struct {
	Text        string  // The plain text portion of the message
	HTML        string  // The HTML portion of the message
	Root        *Part   // The top-level Part
	Attachments []*Part // All parts having a Content-Disposition of attachment
	Inlines     []*Part // All parts having a Content-Disposition of inline
	// All non-text parts that were not placed in Attachments or Inlines, such as multipart/related
	// content.
	OtherParts []*Part
	Errors     []*Error // Errors encountered while parsing
	// contains filtered or unexported fields
}

Envelope is a simplified wrapper for MIME email messages.

Example

ExampleEnvelope demonstrates the relationship between Envelope and Parts.

package main

import (
	"fmt"
	"strings"

	"github.com/xhbase/enmime"
)

func main() {
	// Create sample message in memory
	raw := `From: user@inbucket.org
Subject: Example message
Content-Type: multipart/alternative; boundary=Enmime-100

--Enmime-100
Content-Type: text/plain
X-Comment: part1

hello!
--Enmime-100
Content-Type: text/html
X-Comment: part2

<b>hello!</b>
--Enmime-100
Content-Type: text/plain
Content-Disposition: attachment;
filename=hi.txt
X-Comment: part3

hello again!
--Enmime-100--
`

	// Parse message body with enmime.ReadEnvelope
	r := strings.NewReader(raw)
	env, err := enmime.ReadEnvelope(r)
	if err != nil {
		fmt.Print(err)
		return
	}

	// The root Part contains the message header, which is also available via the
	// Envelope.GetHeader() method.
	fmt.Printf("Root Part Subject: %q\n", env.Root.Header.Get("Subject"))
	fmt.Printf("Envelope Subject: %q\n", env.GetHeader("Subject"))
	fmt.Println()

	// The text from part1 is consumed and placed into the Envelope.Text field.
	fmt.Printf("Text Content: %q\n", env.Text)

	// But part1 is also available as a child of the root Part.  Only the headers may be accessed,
	// because the content has been consumed.
	part1 := env.Root.FirstChild
	fmt.Printf("Part 1 X-Comment: %q\n", part1.Header.Get("X-Comment"))
	fmt.Println()

	// The HTML from part2 is consumed and placed into the Envelope.HTML field.
	fmt.Printf("HTML Content: %q\n", env.HTML)

	// And part2 is available as the second child of the root Part. Only the headers may be
	// accessed, because the content has been consumed.
	part2 := env.Root.FirstChild.NextSibling
	fmt.Printf("Part 2 X-Comment: %q\n", part2.Header.Get("X-Comment"))
	fmt.Println()

	// Because part3 has a disposition of attachment, it is added to the Envelope.Attachments
	// slice
	fmt.Printf("Attachment 1 X-Comment: %q\n", env.Attachments[0].Header.Get("X-Comment"))

	// And is still available as the third child of the root Part
	part3 := env.Root.FirstChild.NextSibling.NextSibling
	fmt.Printf("Part 3 X-Comment: %q\n", part3.Header.Get("X-Comment"))

	// The content of Attachments, Inlines and OtherParts are available as a slice of bytes
	fmt.Printf("Part 3 Content: %q\n", part3.Content)

	// part3 contained a malformed header line, enmime has attached an Error to it
	p3error := part3.Errors[0]
	fmt.Println(p3error.Error())
	fmt.Println()

	// All Part errors are collected and placed into Envelope.Errors
	fmt.Println("Envelope errors:")
	for _, e := range env.Errors {
		fmt.Println(e.Error())
	}

}
Output:

Root Part Subject: "Example message"
Envelope Subject: "Example message"

Text Content: "hello!"
Part 1 X-Comment: "part1"

HTML Content: "<b>hello!</b>"
Part 2 X-Comment: "part2"

Attachment 1 X-Comment: "part3"
Part 3 X-Comment: "part3"
Part 3 Content: "hello again!"
[W] Malformed Header: Continued line "filename=hi.txt" was not indented

Envelope errors:
[W] Malformed Header: Continued line "filename=hi.txt" was not indented

func EnvelopeFromPart

func EnvelopeFromPart(root *Part) (*Envelope, error)

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.

func ReadEnvelope

func ReadEnvelope(r io.Reader) (*Envelope, error)

ReadEnvelope is a wrapper around ReadParts and EnvelopeFromPart. It parses the content of the provided reader into 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 Envelope.Errors slice.

Example
package main

import (
	"fmt"
	"os"

	"github.com/xhbase/enmime"
)

func main() {
	// Open a sample message file.
	r, err := os.Open("testdata/mail/qp-utf8-header.raw")
	if err != nil {
		fmt.Print(err)
		return
	}

	// Parse message body with enmime.
	env, err := enmime.ReadEnvelope(r)
	if err != nil {
		fmt.Print(err)
		return
	}

	// Headers can be retrieved via Envelope.GetHeader(name).
	fmt.Printf("From: %v\n", env.GetHeader("From"))

	// Address-type headers can be parsed into a list of decoded mail.Address structs.
	alist, _ := env.AddressList("To")
	for _, addr := range alist {
		fmt.Printf("To: %s <%s>\n", addr.Name, addr.Address)
	}

	// enmime can decode quoted-printable headers.
	fmt.Printf("Subject: %v\n", env.GetHeader("Subject"))

	// The plain text body is available as mime.Text.
	fmt.Printf("Text Body: %v chars\n", len(env.Text))

	// The HTML body is stored in mime.HTML.
	fmt.Printf("HTML Body: %v chars\n", len(env.HTML))

	// mime.Inlines is a slice of inlined attacments.
	fmt.Printf("Inlines: %v\n", len(env.Inlines))

	// mime.Attachments contains the non-inline attachments.
	fmt.Printf("Attachments: %v\n", len(env.Attachments))

}
Output:

From: James Hillyerd <jamehi03@jamehi03lx.noa.com>, André Pirard <PIRARD@vm1.ulg.ac.be>
To: Mirosław Marczak <marczak@inbucket.com>
Subject: MIME UTF8 Test ¢ More Text
Text Body: 1300 chars
HTML Body: 1736 chars
Inlines: 0
Attachments: 0

func (*Envelope) AddHeader added in v1.0.3

func (e *Envelope) AddHeader(name string, value string) error

AddHeader appends given header value to header name without changing existing values. If the header does not exist already, it will be created.

func (*Envelope) AddressList

func (e *Envelope) AddressList(key string) ([]*mail.Address, error)

AddressList returns a mail.Address slice with RFC 2047 encoded names converted to UTF-8

func (*Envelope) Clone added in v1.0.3

func (e *Envelope) Clone() *Envelope

Clone returns a clone of the current Envelope

func (*Envelope) DeleteHeader added in v1.0.3

func (e *Envelope) DeleteHeader(name string) error

DeleteHeader deletes given header.

func (*Envelope) GetHeader

func (e *Envelope) GetHeader(name string) string

GetHeader processes the specified header for RFC 2047 encoded words and returns the result as a UTF-8 string

func (*Envelope) GetHeaderKeys added in v1.0.3

func (e *Envelope) GetHeaderKeys() (headers []string)

GetHeaderKeys returns a list of header keys seen in this message. Get individual headers with `GetHeader(name)`

Example
package main

import (
	"fmt"
	"os"
	"sort"

	"github.com/xhbase/enmime"
)

func main() {
	// Open a sample message file.
	r, err := os.Open("testdata/mail/qp-utf8-header.raw")
	if err != nil {
		fmt.Print(err)
		return
	}

	// Parse message with enmime.
	env, err := enmime.ReadEnvelope(r)
	if err != nil {
		fmt.Print(err)
		return
	}

	// A list of headers is retrieved via Envelope.GetHeaderKeys().
	headers := env.GetHeaderKeys()
	sort.Sort(sort.StringSlice(headers))

	// Print each header, key and value.
	for _, header := range headers {
		fmt.Printf("%s: %v\n", header, env.GetHeader(header))
	}

}
Output:

Content-Type: multipart/alternative; boundary="------------020203040006070307010003"
Date: Fri, 19 Oct 2012 12:22:49 -0700
From: James Hillyerd <jamehi03@jamehi03lx.noa.com>, André Pirard <PIRARD@vm1.ulg.ac.be>
Message-Id: <5081A889.3020108@jamehi03lx.noa.com>
Mime-Version: 1.0
Sender: André Pirard <PIRARD@vm1.ulg.ac.be>
Subject: MIME UTF8 Test ¢ More Text
To: Mirosław Marczak <marczak@inbucket.com>
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1

func (*Envelope) GetHeaderValues added in v1.0.3

func (e *Envelope) GetHeaderValues(name string) []string

GetHeaderValues processes the specified header for RFC 2047 encoded words and returns all existing values as a list of UTF-8 strings

func (*Envelope) SetHeader added in v1.0.3

func (e *Envelope) SetHeader(name string, value []string) error

SetHeader sets given header name to the given value. If the header exists already, all existing values are replaced.

type Error

type Error struct {
	Name   string // The name or type of error encountered, from Error consts.
	Detail string // Additional detail about the cause of the error, if available.
	Severe bool   // Indicates that a portion of the message was lost during parsing.
}

Error describes an error encountered while parsing.

func (*Error) Error added in v1.0.3

func (e *Error) Error() string

Error formats the enmime.Error as a string.

func (*Error) String

func (e *Error) String() string

String formats the enmime.Error as a string. DEPRECATED; use Error() instead.

type MailBuilder

type MailBuilder struct {
	// contains filtered or unexported fields
}

MailBuilder facilitates the easy construction of a MIME message. Each manipulation method returns a copy of the receiver struct. It can be considered immutable if the caller does not modify the string and byte slices passed in. Immutability allows the headers or entire message to be reused across multiple threads.

func Builder

func Builder() MailBuilder

Builder returns an empty MailBuilder struct.

Example

ExampleBuilder illustrates how to build and send a MIME encoded message.

package main

import (
	"net/smtp"

	"github.com/xhbase/enmime"
)

func main() {
	smtpHost := "smtp.relay.host:25"
	smtpAuth := smtp.PlainAuth("", "user", "pw", "host")

	// MailBuilder is (mostly) immutable, each method below returns a new MailBuilder without
	// modifying the original.
	master := enmime.Builder().
		From("Do Not Reply", "noreply@inbucket.org").
		Subject("Inbucket Newsletter").
		Text([]byte("Text body")).
		HTML([]byte("<p>HTML body</p>"))

	// master is immutable, causing each msg below to have a single recipient.
	msg := master.To("Esteemed Customer", "user1@inbucket.org")
	msg.Send(smtpHost, smtpAuth)

	msg = master.To("Another Customer", "user2@inbucket.org")
	msg.Send(smtpHost, smtpAuth)
}
Output:

func (MailBuilder) AddAttachment

func (p MailBuilder) AddAttachment(b []byte, contentType string, fileName string) MailBuilder

AddAttachment returns a copy of MailBuilder that includes the specified attachment.

func (MailBuilder) AddFileAttachment

func (p MailBuilder) AddFileAttachment(path string) MailBuilder

AddFileAttachment returns a copy of MailBuilder that includes the specified attachment. fileName, will be populated from the base name of path. Content type will be detected from the path extension.

func (MailBuilder) AddFileInline

func (p MailBuilder) AddFileInline(path string) MailBuilder

AddFileInline returns a copy of MailBuilder that includes the specified inline. fileName and contentID will be populated from the base name of path. Content type will be detected from the path extension.

func (MailBuilder) AddInline

func (p MailBuilder) AddInline(
	b []byte,
	contentType string,
	fileName string,
	contentID string,
) MailBuilder

AddInline returns a copy of MailBuilder that includes the specified inline. fileName and contentID may be left empty.

func (MailBuilder) BCC

func (p MailBuilder) BCC(name, addr string) MailBuilder

BCC returns a copy of MailBuilder with this name & address appended to the BCC list. name may be empty. This method only has an effect if the Send method is used to transmit the message, there is no effect on the parts returned by Build().

func (MailBuilder) BCCAddrs

func (p MailBuilder) BCCAddrs(bcc []mail.Address) MailBuilder

BCCAddrs returns a copy of MailBuilder with the specified as the blind CC list. This method only has an effect if the Send method is used to transmit the message, there is no effect on the parts returned by Build().

func (MailBuilder) Build

func (p MailBuilder) Build() (*Part, error)

Build performs some basic validations, then constructs a tree of Part structs from the configured MailBuilder. It will set the Date header to now if it was not explicitly set.

func (MailBuilder) BuildDraft added in v1.0.3

func (p MailBuilder) BuildDraft() (*Part, error)

Build performs some basic validations, then constructs a tree of Part structs from the configured MailBuilder. It will set the Date header to now if it was not explicitly set.

func (MailBuilder) CC

func (p MailBuilder) CC(name, addr string) MailBuilder

CC returns a copy of MailBuilder with this name & address appended to the CC header. name may be empty.

func (MailBuilder) CCAddrs

func (p MailBuilder) CCAddrs(cc []mail.Address) MailBuilder

CCAddrs returns a copy of MailBuilder with the specified CC addresses.

func (MailBuilder) Date

func (p MailBuilder) Date(date time.Time) MailBuilder

Date returns a copy of MailBuilder with the specified Date header.

func (MailBuilder) Equals

func (p MailBuilder) Equals(o MailBuilder) bool

Equals uses the reflect package to test two MailBuilder structs for equality, primarily for unit tests.

func (MailBuilder) Error

func (p MailBuilder) Error() error

Error returns the stored error from a file attachment/inline read or nil.

func (MailBuilder) From

func (p MailBuilder) From(name, addr string) MailBuilder

From returns a copy of MailBuilder with the specified From header.

func (MailBuilder) HTML

func (p MailBuilder) HTML(body []byte) MailBuilder

HTML returns a copy of MailBuilder that will use the provided bytes for its text/html Part.

func (MailBuilder) Header

func (p MailBuilder) Header(name, value string) MailBuilder

Header returns a copy of MailBuilder with the specified value added to the named header.

func (MailBuilder) ReplyTo

func (p MailBuilder) ReplyTo(name, addr string) MailBuilder

ReplyTo returns a copy of MailBuilder with this name & address appended to the To header. name may be empty.

func (MailBuilder) Send

func (p MailBuilder) Send(addr string, a smtp.Auth) error

Send encodes the message and sends it via the SMTP server specified by addr. Send uses net/smtp.SendMail, and accepts the same authentication parameters.

func (MailBuilder) Subject

func (p MailBuilder) Subject(subject string) MailBuilder

Subject returns a copy of MailBuilder with the specified Subject header.

func (MailBuilder) Text

func (p MailBuilder) Text(body []byte) MailBuilder

Text returns a copy of MailBuilder that will use the provided bytes for its text/plain Part.

func (MailBuilder) To

func (p MailBuilder) To(name, addr string) MailBuilder

To returns a copy of MailBuilder with this name & address appended to the To header. name may be empty.

func (MailBuilder) ToAddrs

func (p MailBuilder) ToAddrs(to []mail.Address) MailBuilder

ToAddrs returns a copy of MailBuilder with the specified To addresses.

type Part

type Part struct {
	PartID      string               // PartID labels this parts position within the tree.
	Parent      *Part                // Parent of this part (can be nil.)
	FirstChild  *Part                // FirstChild is the top most child of this part.
	NextSibling *Part                // NextSibling of this part.
	Header      textproto.MIMEHeader // Header for this Part.

	Boundary          string            // Boundary marker used within this part.
	ContentID         string            // ContentID header for cid URL scheme.
	ContentType       string            // ContentType header without parameters.
	ContentTypeParams map[string]string // Params, added to ContentType header.
	Disposition       string            // Content-Disposition header without parameters.
	FileName          string            // The file-name from disposition or type header.
	FileModDate       time.Time         // The modification date of the file.
	Charset           string            // The content charset encoding, may differ from charset in header.
	OrigCharset       string            // The original content charset when a different charset was detected.

	Errors   []*Error // Errors encountered while parsing this part.
	Content  []byte   // Content after decoding, UTF-8 conversion if applicable.
	Epilogue []byte   // Epilogue contains data following the closing boundary marker.
}

Part represents a node in the MIME multipart tree. The Content-Type, Disposition and File Name are parsed out of the header for easier access.

func NewPart

func NewPart(contentType string) *Part

NewPart creates a new Part object.

func ReadParts

func ReadParts(r io.Reader) (*Part, error)

ReadParts reads a MIME document from the provided reader and parses it into tree of Part objects.

func (*Part) AddChild

func (p *Part) AddChild(child *Part)

AddChild adds a child part to either FirstChild or the end of the children NextSibling chain. The child may have siblings and children attached. This method will set the Parent field on child and all its siblings. Safe to call on nil.

func (*Part) BreadthMatchAll

func (p *Part) BreadthMatchAll(matcher PartMatcher) []*Part

BreadthMatchAll performs a breadth first search of the Part tree and returns all parts that cause the given matcher to return true

func (*Part) BreadthMatchFirst

func (p *Part) BreadthMatchFirst(matcher PartMatcher) *Part

BreadthMatchFirst performs a breadth first search of the Part tree and returns the first part that causes the given matcher to return true

func (*Part) Clone added in v1.0.3

func (p *Part) Clone(parent *Part) *Part

Clone returns a clone of the current Part.

func (*Part) DepthMatchAll

func (p *Part) DepthMatchAll(matcher PartMatcher) []*Part

DepthMatchAll performs a depth first search of the Part tree and returns all parts that causes the given matcher to return true

func (*Part) DepthMatchFirst

func (p *Part) DepthMatchFirst(matcher PartMatcher) *Part

DepthMatchFirst performs a depth first search of the Part tree and returns the first part that causes the given matcher to return true

func (*Part) Encode

func (p *Part) Encode(writer io.Writer) error

Encode writes this Part and all its children to the specified writer in MIME format.

func (*Part) TextContent

func (p *Part) TextContent() bool

TextContent indicates whether the content is text based on its content type. This value determines what content transfer encoding scheme to use.

type PartMatcher

type PartMatcher func(part *Part) bool

PartMatcher is a function type that you must implement to search for Parts using the BreadthMatch* functions. Implementators should inspect the provided Part and return true if it matches your criteria.

Directories

Path Synopsis
cmd
mime-dump
Package main outputs a markdown formatted document describing the provided email
Package main outputs a markdown formatted document describing the provided email
mime-extractor
Package main extracts attachments from the provided email
Package main extracts attachments from the provided email
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL