372 lines
10 KiB
Go
372 lines
10 KiB
Go
package enmime
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io/ioutil"
|
|
"mime"
|
|
"net/mail"
|
|
"net/textproto"
|
|
"path/filepath"
|
|
"reflect"
|
|
"time"
|
|
|
|
"github.com/jhillyerd/enmime/internal/stringutil"
|
|
)
|
|
|
|
// 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.
|
|
type MailBuilder struct {
|
|
to, cc, bcc []mail.Address
|
|
from mail.Address
|
|
replyTo mail.Address
|
|
subject string
|
|
date time.Time
|
|
header textproto.MIMEHeader
|
|
text, html []byte
|
|
inlines, attachments []*Part
|
|
err error
|
|
}
|
|
|
|
// Builder returns an empty MailBuilder struct.
|
|
func Builder() MailBuilder {
|
|
return MailBuilder{}
|
|
}
|
|
|
|
// Error returns the stored error from a file attachment/inline read or nil.
|
|
func (p MailBuilder) Error() error {
|
|
return p.err
|
|
}
|
|
|
|
// Date returns a copy of MailBuilder with the specified Date header.
|
|
func (p MailBuilder) Date(date time.Time) MailBuilder {
|
|
p.date = date
|
|
return p
|
|
}
|
|
|
|
// From returns a copy of MailBuilder with the specified From header.
|
|
func (p MailBuilder) From(name, addr string) MailBuilder {
|
|
p.from = mail.Address{Name: name, Address: addr}
|
|
return p
|
|
}
|
|
|
|
// Subject returns a copy of MailBuilder with the specified Subject header.
|
|
func (p MailBuilder) Subject(subject string) MailBuilder {
|
|
p.subject = subject
|
|
return p
|
|
}
|
|
|
|
// To returns a copy of MailBuilder with this name & address appended to the To header. name may be
|
|
// empty.
|
|
func (p MailBuilder) To(name, addr string) MailBuilder {
|
|
if len(addr) > 0 {
|
|
p.to = append(p.to, mail.Address{Name: name, Address: addr})
|
|
}
|
|
return p
|
|
}
|
|
|
|
// ToAddrs returns a copy of MailBuilder with the specified To addresses.
|
|
func (p MailBuilder) ToAddrs(to []mail.Address) MailBuilder {
|
|
p.to = to
|
|
return p
|
|
}
|
|
|
|
// CC returns a copy of MailBuilder with this name & address appended to the CC header. name may be
|
|
// empty.
|
|
func (p MailBuilder) CC(name, addr string) MailBuilder {
|
|
if len(addr) > 0 {
|
|
p.cc = append(p.cc, mail.Address{Name: name, Address: addr})
|
|
}
|
|
return p
|
|
}
|
|
|
|
// CCAddrs returns a copy of MailBuilder with the specified CC addresses.
|
|
func (p MailBuilder) CCAddrs(cc []mail.Address) MailBuilder {
|
|
p.cc = cc
|
|
return p
|
|
}
|
|
|
|
// 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 (p MailBuilder) BCC(name, addr string) MailBuilder {
|
|
if len(addr) > 0 {
|
|
p.bcc = append(p.bcc, mail.Address{Name: name, Address: addr})
|
|
}
|
|
return p
|
|
}
|
|
|
|
// 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 (p MailBuilder) BCCAddrs(bcc []mail.Address) MailBuilder {
|
|
p.bcc = bcc
|
|
return p
|
|
}
|
|
|
|
// ReplyTo returns a copy of MailBuilder with this name & address appended to the To header. name
|
|
// may be empty.
|
|
func (p MailBuilder) ReplyTo(name, addr string) MailBuilder {
|
|
p.replyTo = mail.Address{Name: name, Address: addr}
|
|
return p
|
|
}
|
|
|
|
// Header returns a copy of MailBuilder with the specified value added to the named header.
|
|
func (p MailBuilder) Header(name, value string) MailBuilder {
|
|
// Copy existing header map
|
|
h := textproto.MIMEHeader{}
|
|
for k, v := range p.header {
|
|
h[k] = v
|
|
}
|
|
h.Add(name, value)
|
|
p.header = h
|
|
return p
|
|
}
|
|
|
|
// Text returns a copy of MailBuilder that will use the provided bytes for its text/plain Part.
|
|
func (p MailBuilder) Text(body []byte) MailBuilder {
|
|
p.text = body
|
|
return p
|
|
}
|
|
|
|
// HTML returns a copy of MailBuilder that will use the provided bytes for its text/html Part.
|
|
func (p MailBuilder) HTML(body []byte) MailBuilder {
|
|
p.html = body
|
|
return p
|
|
}
|
|
|
|
// AddAttachment returns a copy of MailBuilder that includes the specified attachment.
|
|
func (p MailBuilder) AddAttachment(b []byte, contentType string, fileName string) MailBuilder {
|
|
part := NewPart(contentType)
|
|
part.Content = b
|
|
part.FileName = fileName
|
|
part.Disposition = cdAttachment
|
|
p.attachments = append(p.attachments, part)
|
|
return p
|
|
}
|
|
|
|
// 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 (p MailBuilder) AddFileAttachment(path string) MailBuilder {
|
|
// Only allow first p.err value
|
|
if p.err != nil {
|
|
return p
|
|
}
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
p.err = err
|
|
return p
|
|
}
|
|
name := filepath.Base(path)
|
|
ctype := mime.TypeByExtension(filepath.Ext(name))
|
|
return p.AddAttachment(b, ctype, name)
|
|
}
|
|
|
|
// AddInline returns a copy of MailBuilder that includes the specified inline. fileName and
|
|
// contentID may be left empty.
|
|
func (p MailBuilder) AddInline(
|
|
b []byte,
|
|
contentType string,
|
|
fileName string,
|
|
contentID string,
|
|
) MailBuilder {
|
|
part := NewPart(contentType)
|
|
part.Content = b
|
|
part.FileName = fileName
|
|
part.Disposition = cdInline
|
|
part.ContentID = contentID
|
|
p.inlines = append(p.inlines, part)
|
|
return p
|
|
}
|
|
|
|
// 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 (p MailBuilder) AddFileInline(path string) MailBuilder {
|
|
// Only allow first p.err value
|
|
if p.err != nil {
|
|
return p
|
|
}
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
p.err = err
|
|
return p
|
|
}
|
|
name := filepath.Base(path)
|
|
ctype := mime.TypeByExtension(filepath.Ext(name))
|
|
return p.AddInline(b, ctype, name, name)
|
|
}
|
|
|
|
// AddOtherPart returns a copy of MailBuilder that includes the specified embedded part.
|
|
// fileName may be left empty.
|
|
// It's useful when you want to embed image with CID.
|
|
func (p MailBuilder) AddOtherPart(
|
|
b []byte,
|
|
contentType string,
|
|
fileName string,
|
|
contentID string,
|
|
) MailBuilder {
|
|
part := NewPart(contentType)
|
|
part.Content = b
|
|
part.FileName = fileName
|
|
part.ContentID = contentID
|
|
p.inlines = append(p.inlines, part)
|
|
return p
|
|
}
|
|
|
|
// AddFileOtherPart returns a copy of MailBuilder that includes the specified other part.
|
|
// Filename and contentID will be populated from the base name of path.
|
|
// Content type will be detected from the path extension.
|
|
func (p MailBuilder) AddFileOtherPart(path string) MailBuilder {
|
|
// Only allow first p.err value
|
|
if p.err != nil {
|
|
return p
|
|
}
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
p.err = err
|
|
return p
|
|
}
|
|
name := filepath.Base(path)
|
|
ctype := mime.TypeByExtension(filepath.Ext(name))
|
|
return p.AddOtherPart(b, ctype, name, name)
|
|
}
|
|
|
|
// 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 (p MailBuilder) Build() (*Part, error) {
|
|
if p.err != nil {
|
|
return nil, p.err
|
|
}
|
|
// Validations
|
|
if p.from.Address == "" {
|
|
return nil, errors.New("from not set")
|
|
}
|
|
if len(p.to)+len(p.cc)+len(p.bcc) == 0 {
|
|
return nil, errors.New(ErrorMissingRecipient)
|
|
}
|
|
// Fully loaded structure; the presence of text, html, inlines, and attachments will determine
|
|
// how much is necessary:
|
|
//
|
|
// multipart/mixed
|
|
// |- multipart/related
|
|
// | |- multipart/alternative
|
|
// | | |- text/plain
|
|
// | | `- text/html
|
|
// | |- other parts..
|
|
// | `- inlines..
|
|
// `- attachments..
|
|
//
|
|
// We build this tree starting at the leaves, re-rooting as needed.
|
|
var root, part *Part
|
|
if p.text != nil || p.html == nil {
|
|
root = NewPart(ctTextPlain)
|
|
root.Content = p.text
|
|
root.Charset = utf8
|
|
}
|
|
if p.html != nil {
|
|
part = NewPart(ctTextHTML)
|
|
part.Content = p.html
|
|
part.Charset = utf8
|
|
if root == nil {
|
|
root = part
|
|
} else {
|
|
root.NextSibling = part
|
|
}
|
|
}
|
|
if p.text != nil && p.html != nil {
|
|
// Wrap Text & HTML bodies
|
|
part = root
|
|
root = NewPart(ctMultipartAltern)
|
|
root.AddChild(part)
|
|
}
|
|
if len(p.inlines) > 0 {
|
|
part = root
|
|
root = NewPart(ctMultipartRelated)
|
|
root.AddChild(part)
|
|
for _, ip := range p.inlines {
|
|
// Copy inline/other part to isolate mutations
|
|
part = &Part{}
|
|
*part = *ip
|
|
part.Header = make(textproto.MIMEHeader)
|
|
root.AddChild(part)
|
|
}
|
|
}
|
|
if len(p.attachments) > 0 {
|
|
part = root
|
|
root = NewPart(ctMultipartMixed)
|
|
root.AddChild(part)
|
|
for _, ap := range p.attachments {
|
|
// Copy attachment Part to isolate mutations
|
|
part = &Part{}
|
|
*part = *ap
|
|
part.Header = make(textproto.MIMEHeader)
|
|
root.AddChild(part)
|
|
}
|
|
}
|
|
// Headers
|
|
h := root.Header
|
|
h.Set(hnMIMEVersion, "1.0")
|
|
h.Set("From", p.from.String())
|
|
h.Set("Subject", p.subject)
|
|
if len(p.to) > 0 {
|
|
h.Set("To", stringutil.JoinAddress(p.to))
|
|
}
|
|
if len(p.cc) > 0 {
|
|
h.Set("Cc", stringutil.JoinAddress(p.cc))
|
|
}
|
|
if p.replyTo.Address != "" {
|
|
h.Set("Reply-To", p.replyTo.String())
|
|
}
|
|
date := p.date
|
|
if date.IsZero() {
|
|
date = time.Now()
|
|
}
|
|
h.Set("Date", date.Format(time.RFC1123Z))
|
|
for k, v := range p.header {
|
|
for _, s := range v {
|
|
h.Add(k, s)
|
|
}
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
// SendWithReversePath encodes the message and sends it via the specified Sender.
|
|
func (p MailBuilder) SendWithReversePath(sender Sender, from string) error {
|
|
buf := &bytes.Buffer{}
|
|
root, err := p.Build()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = root.Encode(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recips := make([]string, 0, len(p.to)+len(p.cc)+len(p.bcc))
|
|
for _, a := range p.to {
|
|
recips = append(recips, a.Address)
|
|
}
|
|
for _, a := range p.cc {
|
|
recips = append(recips, a.Address)
|
|
}
|
|
for _, a := range p.bcc {
|
|
recips = append(recips, a.Address)
|
|
}
|
|
return sender.Send(from, recips, buf.Bytes())
|
|
}
|
|
|
|
// Send encodes the message and sends it via the specified Sender, using the address provided to
|
|
// `From()` as the reverse-path.
|
|
func (p MailBuilder) Send(sender Sender) error {
|
|
return p.SendWithReversePath(sender, p.from.Address)
|
|
}
|
|
|
|
// Equals uses the reflect package to test two MailBuilder structs for equality, primarily for unit
|
|
// tests.
|
|
func (p MailBuilder) Equals(o MailBuilder) bool {
|
|
return reflect.DeepEqual(p, o)
|
|
}
|