go-mail/msg.go

982 lines
28 KiB
Go
Raw Normal View History

// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
//
// SPDX-License-Identifier: MIT
2022-03-09 16:52:23 +01:00
package mail
import (
"bytes"
"context"
"embed"
"errors"
2022-03-09 16:52:23 +01:00
"fmt"
ht "html/template"
"io"
2022-03-09 16:52:23 +01:00
"math/rand"
"mime"
2022-03-10 16:19:51 +01:00
"net/mail"
2022-03-09 16:52:23 +01:00
"os"
"os/exec"
2022-03-14 10:29:53 +01:00
"path/filepath"
"syscall"
tt "text/template"
2022-03-09 16:52:23 +01:00
"time"
)
var (
// ErrNoFromAddress should be used when a FROM address is requrested but not set
ErrNoFromAddress = errors.New("no FROM address set")
// ErrNoRcptAddresses should be used when the list of RCPTs is empty
ErrNoRcptAddresses = errors.New("no recipient addresses set")
)
const (
// errTplExecuteFailed is issued when the template execution was not successful
errTplExecuteFailed = "failed to execute template: %w"
// errTplPointerNil is issued when a template pointer is expected but it is nil
errTplPointerNil = "template pointer is nil"
// errParseMailAddr is used when a mail address could not be validated
errParseMailAddr = "failed to parse mail address %q: %w"
)
// Middleware is an interface to define a function to apply to Msg before sending
type Middleware interface {
Handle(*Msg) *Msg
}
2022-03-09 16:52:23 +01:00
// Msg is the mail message struct
type Msg struct {
// addrHeader is a slice of strings that the different mail AddrHeader fields
addrHeader map[AddrHeader][]*mail.Address
// attachments represent the different attachment File of the Msg
attachments []*File
// boundary is the MIME content boundary
boundary string
2022-03-09 16:52:23 +01:00
// charset represents the charset of the mail (defaults to UTF-8)
charset Charset
// embeds represent the different embedded File of the Msg
embeds []*File
2022-03-09 16:52:23 +01:00
// encoder represents a mime.WordEncoder from the std lib
encoder mime.WordEncoder
2022-03-09 16:52:23 +01:00
// encoding represents the message encoding (the encoder will be a corresponding WordEncoder)
encoding Encoding
2022-03-10 16:19:51 +01:00
// genHeader is a slice of strings that the different generic mail Header fields
genHeader map[Header][]string
// mimever represents the MIME version
mimever MIMEVersion
// parts represent the different parts of the Msg
parts []*Part
// middlewares is the list of middlewares to apply to the Msg before sending in FIFO order
middlewares []Middleware
2022-03-14 10:29:53 +01:00
}
// SendmailPath is the default system path to the sendmail binary
const SendmailPath = "/usr/sbin/sendmail"
// MsgOption returns a function that can be used for grouping Msg options
type MsgOption func(*Msg)
2022-03-09 16:52:23 +01:00
// NewMsg returns a new Msg pointer
func NewMsg(o ...MsgOption) *Msg {
2022-03-09 16:52:23 +01:00
m := &Msg{
addrHeader: make(map[AddrHeader][]*mail.Address),
charset: CharsetUTF8,
encoding: EncodingQP,
2022-03-10 16:19:51 +01:00
genHeader: make(map[Header][]string),
mimever: Mime10,
2022-03-09 16:52:23 +01:00
}
// Override defaults with optionally provided MsgOption functions
for _, co := range o {
if co == nil {
continue
}
co(m)
}
// Set the matcing mime.WordEncoder for the Msg
m.setEncoder()
2022-03-09 16:52:23 +01:00
return m
}
// WithCharset overrides the default message charset
func WithCharset(c Charset) MsgOption {
return func(m *Msg) {
m.charset = c
}
}
// WithEncoding overrides the default message encoding
func WithEncoding(e Encoding) MsgOption {
return func(m *Msg) {
m.encoding = e
}
}
// WithMIMEVersion overrides the default MIME version
func WithMIMEVersion(mv MIMEVersion) MsgOption {
return func(m *Msg) {
m.mimever = mv
}
}
2022-03-18 11:47:50 +01:00
// WithBoundary overrides the default MIME boundary
func WithBoundary(b string) MsgOption {
return func(m *Msg) {
m.boundary = b
}
}
// WithMiddleware add the given middleware in the end of the list of the client middlewares
func WithMiddleware(mw Middleware) MsgOption {
return func(m *Msg) {
m.middlewares = append(m.middlewares, mw)
}
}
// SetCharset sets the encoding charset of the Msg
func (m *Msg) SetCharset(c Charset) {
m.charset = c
}
// SetEncoding sets the encoding of the Msg
func (m *Msg) SetEncoding(e Encoding) {
m.encoding = e
2022-03-13 20:01:02 +01:00
m.setEncoder()
}
// SetBoundary sets the boundary of the Msg
func (m *Msg) SetBoundary(b string) {
m.boundary = b
}
// SetMIMEVersion sets the MIME version of the Msg
func (m *Msg) SetMIMEVersion(mv MIMEVersion) {
m.mimever = mv
}
2022-03-13 10:49:07 +01:00
// Encoding returns the currently set encoding of the Msg
func (m *Msg) Encoding() string {
return m.encoding.String()
}
// Charset returns the currently set charset of the Msg
func (m *Msg) Charset() string {
return m.charset.String()
}
2022-03-10 16:19:51 +01:00
// SetHeader sets a generic header field of the Msg
2022-03-09 16:52:23 +01:00
func (m *Msg) SetHeader(h Header, v ...string) {
for i, hv := range v {
v[i] = m.encodeString(hv)
}
2022-03-10 16:19:51 +01:00
m.genHeader[h] = v
}
// SetAddrHeader sets an address related header field of the Msg
func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error {
var al []*mail.Address
for _, av := range v {
a, err := mail.ParseAddress(av)
2022-03-10 16:19:51 +01:00
if err != nil {
return fmt.Errorf(errParseMailAddr, av, err)
2022-03-10 16:19:51 +01:00
}
al = append(al, a)
}
2022-03-09 16:52:23 +01:00
switch h {
case HeaderFrom:
2022-03-10 16:19:51 +01:00
m.addrHeader[h] = []*mail.Address{al[0]}
2022-03-09 16:52:23 +01:00
default:
2022-03-10 16:19:51 +01:00
m.addrHeader[h] = al
2022-03-09 16:52:23 +01:00
}
2022-03-10 16:19:51 +01:00
return nil
}
// SetAddrHeaderIgnoreInvalid sets an address related header field of the Msg and ignores invalid address
// in the validation process
func (m *Msg) SetAddrHeaderIgnoreInvalid(h AddrHeader, v ...string) {
var al []*mail.Address
for _, av := range v {
a, err := mail.ParseAddress(m.encodeString(av))
2022-03-10 16:19:51 +01:00
if err != nil {
continue
}
al = append(al, a)
}
m.addrHeader[h] = al
}
// EnvelopeFrom takes and validates a given mail address and sets it as envelope "FROM"
// addrHeader of the Msg
func (m *Msg) EnvelopeFrom(f string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, f)
}
// EnvelopeFromFormat takes a name and address, formats them RFC5322 compliant and stores them as
// the envelope FROM address header field
func (m *Msg) EnvelopeFromFormat(n, a string) error {
return m.SetAddrHeader(HeaderEnvelopeFrom, fmt.Sprintf(`"%s" <%s>`, n, a))
}
2022-03-10 16:19:51 +01:00
// From takes and validates a given mail address and sets it as "From" genHeader of the Msg
func (m *Msg) From(f string) error {
return m.SetAddrHeader(HeaderFrom, f)
2022-03-09 16:52:23 +01:00
}
2022-03-12 20:05:43 +01:00
// FromFormat takes a name and address, formats them RFC5322 compliant and stores them as
// the From address header field
func (m *Msg) FromFormat(n, a string) error {
return m.SetAddrHeader(HeaderFrom, fmt.Sprintf(`"%s" <%s>`, n, a))
}
2022-03-10 16:19:51 +01:00
// To takes and validates a given mail address list sets the To: addresses of the Msg
func (m *Msg) To(t ...string) error {
return m.SetAddrHeader(HeaderTo, t...)
2022-03-09 16:52:23 +01:00
}
2022-03-12 20:05:43 +01:00
// AddTo adds an additional address to the To address header field
func (m *Msg) AddTo(t string) error {
2022-03-13 10:49:07 +01:00
return m.addAddr(HeaderTo, t)
2022-03-12 20:05:43 +01:00
}
2022-03-13 10:49:07 +01:00
// AddToFormat takes a name and address, formats them RFC5322 compliant and stores them as
// as additional To address header field
func (m *Msg) AddToFormat(n, a string) error {
return m.addAddr(HeaderTo, fmt.Sprintf(`"%s" <%s>`, n, a))
}
2022-03-10 16:19:51 +01:00
// ToIgnoreInvalid takes and validates a given mail address list sets the To: addresses of the Msg
// Any provided address that is not RFC5322 compliant, will be ignored
func (m *Msg) ToIgnoreInvalid(t ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderTo, t...)
2022-03-09 16:52:23 +01:00
}
2022-03-10 16:19:51 +01:00
// Cc takes and validates a given mail address list sets the Cc: addresses of the Msg
func (m *Msg) Cc(c ...string) error {
return m.SetAddrHeader(HeaderCc, c...)
}
2022-03-13 10:49:07 +01:00
// AddCc adds an additional address to the Cc address header field
func (m *Msg) AddCc(t string) error {
return m.addAddr(HeaderCc, t)
}
// AddCcFormat takes a name and address, formats them RFC5322 compliant and stores them as
// as additional Cc address header field
func (m *Msg) AddCcFormat(n, a string) error {
return m.addAddr(HeaderCc, fmt.Sprintf(`"%s" <%s>`, n, a))
}
2022-03-10 16:19:51 +01:00
// CcIgnoreInvalid takes and validates a given mail address list sets the Cc: addresses of the Msg
// Any provided address that is not RFC5322 compliant, will be ignored
func (m *Msg) CcIgnoreInvalid(c ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderCc, c...)
}
// Bcc takes and validates a given mail address list sets the Bcc: addresses of the Msg
func (m *Msg) Bcc(b ...string) error {
return m.SetAddrHeader(HeaderBcc, b...)
}
2022-03-13 10:49:07 +01:00
// AddBcc adds an additional address to the Bcc address header field
func (m *Msg) AddBcc(t string) error {
return m.addAddr(HeaderBcc, t)
}
// AddBccFormat takes a name and address, formats them RFC5322 compliant and stores them as
// as additional Bcc address header field
func (m *Msg) AddBccFormat(n, a string) error {
return m.addAddr(HeaderBcc, fmt.Sprintf(`"%s" <%s>`, n, a))
}
2022-03-10 16:19:51 +01:00
// BccIgnoreInvalid takes and validates a given mail address list sets the Bcc: addresses of the Msg
// Any provided address that is not RFC5322 compliant, will be ignored
func (m *Msg) BccIgnoreInvalid(b ...string) {
m.SetAddrHeaderIgnoreInvalid(HeaderBcc, b...)
2022-03-09 16:52:23 +01:00
}
2022-03-13 19:36:24 +01:00
// ReplyTo takes and validates a given mail address and sets it as "Reply-To" addrHeader of the Msg
func (m *Msg) ReplyTo(r string) error {
rt, err := mail.ParseAddress(m.encodeString(r))
if err != nil {
return fmt.Errorf("failed to parse reply-to address: %w", err)
}
m.SetHeader(HeaderReplyTo, rt.String())
return nil
}
// ReplyToFormat takes a name and address, formats them RFC5322 compliant and stores them as
// the Reply-To header field
func (m *Msg) ReplyToFormat(n, a string) error {
return m.ReplyTo(fmt.Sprintf(`"%s" <%s>`, n, a))
}
2022-03-13 10:49:07 +01:00
// addAddr adds an additional address to the given addrHeader of the Msg
func (m *Msg) addAddr(h AddrHeader, a string) error {
var al []string
for _, ca := range m.addrHeader[h] {
al = append(al, ca.String())
}
al = append(al, a)
return m.SetAddrHeader(h, al...)
}
// Subject sets the "Subject" header field of the Msg
func (m *Msg) Subject(s string) {
m.SetHeader(HeaderSubject, s)
}
2022-03-09 16:52:23 +01:00
// SetMessageID generates a random message id for the mail
func (m *Msg) SetMessageID() {
hn, err := os.Hostname()
if err != nil {
hn = "localhost.localdomain"
}
ct := time.Now().Unix()
2022-03-09 16:52:23 +01:00
r := rand.New(rand.NewSource(ct))
rn := r.Int()
pid := os.Getpid()
mid := fmt.Sprintf("%d.%d.%d@%s", pid, rn, ct, hn)
m.SetMessageIDWithValue(mid)
}
// SetMessageIDWithValue sets the message id for the mail
func (m *Msg) SetMessageIDWithValue(v string) {
m.SetHeader(HeaderMessageID, fmt.Sprintf("<%s>", v))
2022-03-09 16:52:23 +01:00
}
2022-03-10 16:19:51 +01:00
// SetBulk sets the "Precedence: bulk" genHeader which is recommended for
2022-03-09 16:52:23 +01:00
// automated mails like OOO replies
// See: https://www.rfc-editor.org/rfc/rfc2076#section-3.9
func (m *Msg) SetBulk() {
m.SetHeader(HeaderPrecedence, "bulk")
}
2022-03-10 16:19:51 +01:00
// SetDate sets the Date genHeader field to the current time in a valid format
func (m *Msg) SetDate() {
ts := time.Now().Format(time.RFC1123Z)
m.SetHeader(HeaderDate, ts)
}
// SetDateWithValue sets the Date genHeader field to the provided time in a valid format
func (m *Msg) SetDateWithValue(t time.Time) {
m.SetHeader(HeaderDate, t.Format(time.RFC1123Z))
}
2022-03-13 19:24:46 +01:00
// SetImportance sets the Msg Importance/Priority header to given Importance
func (m *Msg) SetImportance(i Importance) {
if i == ImportanceNormal {
return
}
m.SetHeader(HeaderImportance, i.String())
m.SetHeader(HeaderPriority, i.NumString())
2022-03-13 19:28:40 +01:00
m.SetHeader(HeaderXPriority, i.XPrioString())
2022-03-13 19:24:46 +01:00
m.SetHeader(HeaderXMSMailPriority, i.NumString())
}
// SetOrganization sets the provided string as Organization header for the Msg
func (m *Msg) SetOrganization(o string) {
m.SetHeader(HeaderOrganization, o)
}
// SetUserAgent sets the User-Agent/X-Mailer header for the Msg
func (m *Msg) SetUserAgent(a string) {
m.SetHeader(HeaderUserAgent, a)
m.SetHeader(HeaderXMailer, a)
}
// RequestMDNTo adds the Disposition-Notification-To header to request a MDN from the receiving end
// as described in RFC8098. It allows to provide a list recipient addresses.
// Address validation is performed
// See: https://www.rfc-editor.org/rfc/rfc8098.html
func (m *Msg) RequestMDNTo(t ...string) error {
var tl []string
for _, at := range t {
a, err := mail.ParseAddress(at)
if err != nil {
return fmt.Errorf(errParseMailAddr, at, err)
}
tl = append(tl, a.String())
}
m.genHeader[HeaderDispositionNotificationTo] = tl
return nil
}
// RequestMDNToFormat adds the Disposition-Notification-To header to request a MDN from the receiving end
// as described in RFC8098. It allows to provide a recipient address with name and address and will format
// accordingly. Address validation is performed
// See: https://www.rfc-editor.org/rfc/rfc8098.html
func (m *Msg) RequestMDNToFormat(n, a string) error {
return m.RequestMDNTo(fmt.Sprintf(`%s <%s>`, n, a))
}
// RequestMDNAddTo adds an additional recipient to the recipient list of the MDN
func (m *Msg) RequestMDNAddTo(t string) error {
a, err := mail.ParseAddress(t)
if err != nil {
return fmt.Errorf(errParseMailAddr, t, err)
}
var tl []string
tl = append(tl, m.genHeader[HeaderDispositionNotificationTo]...)
tl = append(tl, a.String())
m.genHeader[HeaderDispositionNotificationTo] = tl
return nil
}
// RequestMDNAddToFormat adds an additional formated recipient to the recipient list of the MDN
func (m *Msg) RequestMDNAddToFormat(n, a string) error {
return m.RequestMDNAddTo(fmt.Sprintf(`"%s" <%s>`, n, a))
}
// GetSender returns the currently set envelope FROM address. If no envelope FROM is set it will use
// the first mail body FROM address. If ff is true, it will return the full address string including
// the address name, if set
func (m *Msg) GetSender(ff bool) (string, error) {
f, ok := m.addrHeader[HeaderEnvelopeFrom]
if !ok || len(f) == 0 {
f, ok = m.addrHeader[HeaderFrom]
if !ok || len(f) == 0 {
return "", ErrNoFromAddress
}
2022-03-11 16:57:14 +01:00
}
if ff {
return f[0].String(), nil
2022-03-11 16:57:14 +01:00
}
return f[0].Address, nil
}
// GetRecipients returns a list of the currently set TO/CC/BCC addresses.
func (m *Msg) GetRecipients() ([]string, error) {
var rl []string
for _, t := range []AddrHeader{HeaderTo, HeaderCc, HeaderBcc} {
al, ok := m.addrHeader[t]
if !ok || len(al) == 0 {
continue
}
for _, r := range al {
rl = append(rl, r.Address)
}
}
if len(rl) <= 0 {
return rl, ErrNoRcptAddresses
}
return rl, nil
}
// GetGenHeader returns the content of the requested generic header of the Msg
func (m *Msg) GetGenHeader(h Header) []string {
return m.genHeader[h]
}
// SetBodyString sets the body of the message.
func (m *Msg) SetBodyString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b)
w := writeFuncFromBuffer(buf)
m.SetBodyWriter(ct, w, o...)
}
// SetBodyWriter sets the body of the message.
func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) (int64, error), o ...PartOption) {
2022-03-14 10:29:53 +01:00
p := m.newPart(ct, o...)
p.w = w
m.parts = []*Part{p}
}
// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer
// The content type will be set to text/html automatically
func (m *Msg) SetBodyHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextHTML, w, o...)
return nil
}
// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer
// The content type will be set to text/plain automatically
func (m *Msg) SetBodyTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextPlain, w, o...)
return nil
}
// AddAlternativeString sets the alternative body of the message.
func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b)
w := writeFuncFromBuffer(buf)
m.AddAlternativeWriter(ct, w, o...)
}
// AddAlternativeWriter sets the body of the message.
func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, error), o ...PartOption) {
2022-03-14 10:29:53 +01:00
p := m.newPart(ct, o...)
p.w = w
m.parts = append(m.parts, p)
}
// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output
// The content type will be set to text/html automatically
func (m *Msg) AddAlternativeHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextHTML, w, o...)
return nil
}
// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output
// The content type will be set to text/plain automatically
func (m *Msg) AddAlternativeTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf(errTplExecuteFailed, err)
}
w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextPlain, w, o...)
return nil
}
2022-03-14 10:29:53 +01:00
// AttachFile adds an attachment File to the Msg
func (m *Msg) AttachFile(n string, o ...FileOption) {
f := fileFromFS(n)
if f == nil {
return
}
m.attachments = m.appendFile(m.attachments, f, o...)
2022-03-14 10:29:53 +01:00
}
// AttachReader adds an attachment File via io.Reader to the Msg
func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) {
f := fileFromReader(n, r)
m.attachments = m.appendFile(m.attachments, f, o...)
2022-03-14 10:29:53 +01:00
}
// AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
f, err := fileFromHTMLTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to attach template: %w", err)
}
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
f, err := fileFromTextTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to attach template: %w", err)
}
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg
func (m *Msg) AttachFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if f == nil {
return fmt.Errorf("embed.FS must not be nil")
}
ef, err := fileFromEmbedFS(n, f)
if err != nil {
return err
}
m.attachments = m.appendFile(m.attachments, ef, o...)
return nil
}
2022-03-14 10:29:53 +01:00
// EmbedFile adds an embedded File to the Msg
func (m *Msg) EmbedFile(n string, o ...FileOption) {
f := fileFromFS(n)
if f == nil {
return
}
m.embeds = m.appendFile(m.embeds, f, o...)
2022-03-14 10:29:53 +01:00
}
// EmbedReader adds an embedded File from an io.Reader to the Msg
func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) {
f := fileFromReader(n, r)
m.embeds = m.appendFile(m.embeds, f, o...)
2022-03-14 10:29:53 +01:00
}
// EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
f, err := fileFromHTMLTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to embed template: %w", err)
}
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
f, err := fileFromTextTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to embed template: %w", err)
}
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg
func (m *Msg) EmbedFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if f == nil {
return fmt.Errorf("embed.FS must not be nil")
}
ef, err := fileFromEmbedFS(n, f)
if err != nil {
return err
}
m.embeds = m.appendFile(m.embeds, ef, o...)
return nil
}
// Reset resets all headers, body parts and attachments/embeds of the Msg
// It leaves already set encodings, charsets, boundaries, etc. as is
func (m *Msg) Reset() {
m.addrHeader = make(map[AddrHeader][]*mail.Address)
m.attachments = nil
m.embeds = nil
m.genHeader = make(map[Header][]string)
m.parts = nil
}
// ApplyMiddlewares apply the list of middlewares to a Msg
func (m *Msg) applyMiddlewares(ms *Msg) *Msg {
for _, mw := range m.middlewares {
ms = mw.Handle(ms)
}
return ms
}
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
func (m *Msg) WriteTo(w io.Writer) (int64, error) {
mw := &msgWriter{w: w, c: m.charset, en: m.encoder}
mw.writeMsg(m.applyMiddlewares(m))
return mw.n, mw.err
2022-03-09 16:52:23 +01:00
}
2022-06-02 16:22:48 +02:00
// Write is an alias method to WriteTo due to compatibility reasons
func (m *Msg) Write(w io.Writer) (int64, error) {
return m.WriteTo(w)
}
2022-03-14 10:29:53 +01:00
// appendFile adds a File to the Msg (as attachment or embed)
func (m *Msg) appendFile(c []*File, f *File, o ...FileOption) []*File {
// Override defaults with optionally provided FileOption functions
for _, co := range o {
if co == nil {
continue
}
co(f)
}
if c == nil {
return []*File{f}
}
return append(c, f)
}
// WriteToFile stores the Msg as file on disk. It will try to create the given filename
// Already existing files will be overwritten
func (m *Msg) WriteToFile(n string) error {
f, err := os.Create(n)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer func() { _ = f.Close() }()
_, err = m.WriteTo(f)
if err != nil {
return fmt.Errorf("failed to write to output file: %w", err)
}
return f.Close()
}
// WriteToSendmail returns WriteToSendmailWithCommand with a default sendmail path
func (m *Msg) WriteToSendmail() error {
return m.WriteToSendmailWithCommand(SendmailPath)
}
// WriteToSendmailWithCommand returns WriteToSendmailWithContext with a default timeout
// of 5 seconds and a given sendmail path
func (m *Msg) WriteToSendmailWithCommand(sp string) error {
tctx, tcfn := context.WithTimeout(context.Background(), time.Second*5)
defer tcfn()
return m.WriteToSendmailWithContext(tctx, sp)
}
// WriteToSendmailWithContext opens an pipe to the local sendmail binary and tries to send the
// mail though that. It takes a context.Context, the path to the sendmail binary and additional
// arguments for the sendmail binary as parameters
func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...string) error {
ec := exec.CommandContext(ctx, sp)
ec.Args = append(ec.Args, "-oi", "-t")
ec.Args = append(ec.Args, a...)
se, err := ec.StderrPipe()
if err != nil {
return fmt.Errorf("failed to set STDERR pipe: %w", err)
}
si, err := ec.StdinPipe()
if err != nil {
return fmt.Errorf("failed to set STDIN pipe: %w", err)
}
// Start the execution and write to STDIN
if err := ec.Start(); err != nil {
return fmt.Errorf("could not start sendmail execution: %w", err)
}
_, err = m.WriteTo(si)
if err != nil {
if !errors.Is(err, syscall.EPIPE) {
return fmt.Errorf("failed to write mail to buffer: %w", err)
}
}
// Close STDIN and wait for completion or cancellation of the sendmail executable
if err := si.Close(); err != nil {
return fmt.Errorf("failed to close STDIN pipe: %w", err)
}
// Read the stderr pipe for possible errors
serr, err := io.ReadAll(se)
if err != nil {
return fmt.Errorf("failed to read STDERR pipe: %w", err)
}
if len(serr) > 0 {
return fmt.Errorf("sendmail command failed: %s", string(serr))
}
if err := ec.Wait(); err != nil {
return fmt.Errorf("sendmail command execution failed: %w", err)
}
return nil
}
// Read outputs the length of p into p to satisfy the io.Reader interface
func (m *Msg) Read(p []byte) (int, error) {
wbuf := bytes.Buffer{}
_, err := m.WriteTo(&wbuf)
if err != nil {
return 0, fmt.Errorf("failed to write message to internal write buffer: %w", err)
}
return wbuf.Read(p)
}
2022-03-14 10:29:53 +01:00
// encodeString encodes a string based on the configured message encoder and the corresponding
// charset for the Msg
func (m *Msg) encodeString(s string) string {
return m.encoder.Encode(string(m.charset), s)
}
// hasAlt returns true if the Msg has more than one part
func (m *Msg) hasAlt() bool {
return len(m.parts) > 1
}
// hasMixed returns true if the Msg has mixed parts
func (m *Msg) hasMixed() bool {
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
}
// hasRelated returns true if the Msg has related parts
func (m *Msg) hasRelated() bool {
return (len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1
}
// newPart returns a new Part for the Msg
func (m *Msg) newPart(ct ContentType, o ...PartOption) *Part {
p := &Part{
ctype: ct,
enc: m.encoding,
}
// Override defaults with optionally provided MsgOption functions
for _, co := range o {
if co == nil {
continue
}
co(p)
}
return p
}
2022-03-14 10:29:53 +01:00
// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func (m *Msg) setEncoder() {
m.encoder = getEncoder(m.encoding)
}
// checkUserAgent checks if a useragent/x-mailer is set and if not will set a default
// version string
func (m *Msg) checkUserAgent() {
_, uaok := m.genHeader[HeaderUserAgent]
_, xmok := m.genHeader[HeaderXMailer]
if !uaok && !xmok {
m.SetUserAgent(fmt.Sprintf("go-mail v%s // https://github.com/wneessen/go-mail",
VERSION))
}
}
// addDefaultHeader sets some default headers, if they haven't been set before
func (m *Msg) addDefaultHeader() {
if _, ok := m.genHeader[HeaderDate]; !ok {
m.SetDate()
}
if _, ok := m.genHeader[HeaderMessageID]; !ok {
m.SetMessageID()
}
m.SetHeader(HeaderMIMEVersion, string(m.mimever))
}
// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS
func fileFromEmbedFS(n string, f *embed.FS) (*File, error) {
_, err := f.Open(n)
if err != nil {
return nil, fmt.Errorf("failed to open file from embed.FS: %w", err)
}
return &File{
Name: filepath.Base(n),
Header: make(map[string][]string),
Writer: func(w io.Writer) (int64, error) {
h, err := f.Open(n)
if err != nil {
return 0, err
}
nb, err := io.Copy(w, h)
if err != nil {
_ = h.Close()
return nb, fmt.Errorf("failed to copy file to io.Writer: %w", err)
}
return nb, h.Close()
},
}, nil
}
2022-03-14 10:29:53 +01:00
// fileFromFS returns a File pointer from a given file in the system's file system
func fileFromFS(n string) *File {
_, err := os.Stat(n)
if err != nil {
return nil
}
2022-03-14 10:29:53 +01:00
return &File{
Name: filepath.Base(n),
Header: make(map[string][]string),
Writer: func(w io.Writer) (int64, error) {
2022-03-14 10:29:53 +01:00
h, err := os.Open(n)
if err != nil {
return 0, err
2022-03-14 10:29:53 +01:00
}
nb, err := io.Copy(w, h)
if err != nil {
2022-03-14 10:29:53 +01:00
_ = h.Close()
return nb, fmt.Errorf("failed to copy file to io.Writer: %w", err)
2022-03-14 10:29:53 +01:00
}
return nb, h.Close()
2022-03-14 10:29:53 +01:00
},
}
}
2022-03-14 10:29:53 +01:00
// fileFromReader returns a File pointer from a given io.Reader
func fileFromReader(n string, r io.Reader) *File {
return &File{
Name: filepath.Base(n),
Header: make(map[string][]string),
Writer: func(w io.Writer) (int64, error) {
nb, err := io.Copy(w, r)
if err != nil {
return nb, err
2022-03-14 10:29:53 +01:00
}
return nb, nil
2022-03-14 10:29:53 +01:00
},
}
}
// fileFromHTMLTemplate returns a File pointer form a given html/template.Template
func fileFromHTMLTemplate(n string, t *ht.Template, d interface{}) (*File, error) {
if t == nil {
return nil, fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf(errTplExecuteFailed, err)
}
f := fileFromReader(n, &buf)
return f, nil
}
// fileFromTextTemplate returns a File pointer form a given text/template.Template
func fileFromTextTemplate(n string, t *tt.Template, d interface{}) (*File, error) {
if t == nil {
return nil, fmt.Errorf(errTplPointerNil)
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf(errTplExecuteFailed, err)
}
f := fileFromReader(n, &buf)
return f, nil
}
// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func getEncoder(e Encoding) mime.WordEncoder {
switch e {
case EncodingQP:
return mime.QEncoding
case EncodingB64:
return mime.BEncoding
default:
return mime.QEncoding
}
}
// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as
// often required by this library
func writeFuncFromBuffer(buf *bytes.Buffer) func(io.Writer) (int64, error) {
w := func(w io.Writer) (int64, error) {
nb, err := w.Write(buf.Bytes())
return int64(nb), err
}
return w
}