363 lines
9.9 KiB
Go
363 lines
9.9 KiB
Go
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||
|
//
|
||
|
// SPDX-License-Identifier: MIT
|
||
|
|
||
|
package mail
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
ht "html/template"
|
||
|
"io"
|
||
|
"mime"
|
||
|
"net/mail"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"syscall"
|
||
|
tt "text/template"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
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"
|
||
|
)
|
||
|
|
||
|
// 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
|
||
|
|
||
|
// boundary is the MIME content boundary
|
||
|
boundary string
|
||
|
|
||
|
// charset represents the charset of the mail (defaults to UTF-8)
|
||
|
charset Charset
|
||
|
|
||
|
// encoder represents a mime.WordEncoder from the std lib
|
||
|
encoder mime.WordEncoder
|
||
|
|
||
|
// encoding represents the message encoding (the encoder will be a corresponding WordEncoder)
|
||
|
encoding Encoding
|
||
|
|
||
|
// 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
|
||
|
|
||
|
// preformHeader is a slice of strings that the different generic mail Header fields
|
||
|
// of which content is already preformatted and will not be affected by the automatic line
|
||
|
// breaks
|
||
|
preformHeader map[Header]string
|
||
|
|
||
|
// sendError holds the SendError in case a Msg could not be delivered during the Client.Send operation
|
||
|
sendError error
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
|
||
|
// NewMsg returns a new Msg pointer
|
||
|
func NewMsg() *Msg {
|
||
|
m := &Msg{
|
||
|
addrHeader: make(map[AddrHeader][]*mail.Address),
|
||
|
charset: CharsetUTF8,
|
||
|
encoding: EncodingQP,
|
||
|
genHeader: make(map[Header][]string),
|
||
|
preformHeader: make(map[Header]string),
|
||
|
mimever: Mime10,
|
||
|
}
|
||
|
|
||
|
// Set the matching mime.WordEncoder for the Msg
|
||
|
m.setEncoder()
|
||
|
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// SetGenHeader sets a generic header field of the Msg
|
||
|
// For adding address headers like "To:" or "From", see SetAddrHeader
|
||
|
func (m *Msg) SetGenHeader(h Header, v ...string) {
|
||
|
if m.genHeader == nil {
|
||
|
m.genHeader = make(map[Header][]string)
|
||
|
}
|
||
|
for i, hv := range v {
|
||
|
v[i] = m.encodeString(hv)
|
||
|
}
|
||
|
m.genHeader[h] = v
|
||
|
}
|
||
|
|
||
|
// SetAddrHeader sets an address related header field of the Msg
|
||
|
func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error {
|
||
|
if m.addrHeader == nil {
|
||
|
m.addrHeader = make(map[AddrHeader][]*mail.Address)
|
||
|
}
|
||
|
var al []*mail.Address
|
||
|
for _, av := range v {
|
||
|
a, err := mail.ParseAddress(av)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf(errParseMailAddr, av, err)
|
||
|
}
|
||
|
al = append(al, a)
|
||
|
}
|
||
|
switch h {
|
||
|
case HeaderFrom:
|
||
|
if len(al) > 0 {
|
||
|
m.addrHeader[h] = []*mail.Address{al[0]}
|
||
|
}
|
||
|
default:
|
||
|
m.addrHeader[h] = al
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
}
|
||
|
|
||
|
// 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...)
|
||
|
}
|
||
|
|
||
|
// 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(r)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("failed to parse reply-to address: %w", err)
|
||
|
}
|
||
|
m.SetGenHeader(HeaderReplyTo, rt.String())
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Subject sets the "Subject" header field of the Msg
|
||
|
func (m *Msg) Subject(s string) {
|
||
|
m.SetGenHeader(HeaderSubject, s)
|
||
|
}
|
||
|
|
||
|
// SetMessageID generates a random message id for the mail
|
||
|
func (m *Msg) SetMessageID() {
|
||
|
hn, err := os.Hostname()
|
||
|
if err != nil {
|
||
|
hn = "localhost.localdomain"
|
||
|
}
|
||
|
rn, _ := randNum(100000000)
|
||
|
rm, _ := randNum(10000)
|
||
|
rs, _ := randomStringSecure(17)
|
||
|
pid := os.Getpid() * rm
|
||
|
mid := fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, hn)
|
||
|
m.SetMessageIDWithValue(mid)
|
||
|
}
|
||
|
|
||
|
// SetMessageIDWithValue sets the message id for the mail
|
||
|
func (m *Msg) SetMessageIDWithValue(v string) {
|
||
|
m.SetGenHeader(HeaderMessageID, fmt.Sprintf("<%s>", v))
|
||
|
}
|
||
|
|
||
|
// 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.SetGenHeader(HeaderDate, ts)
|
||
|
}
|
||
|
|
||
|
// SetBodyWriter sets the body of the message.
|
||
|
func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) (int64, error)) {
|
||
|
p := m.newPart(ct)
|
||
|
p.w = w
|
||
|
m.parts = []*Part{p}
|
||
|
}
|
||
|
|
||
|
// 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{}) 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)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AddAlternativeWriter sets the body of the message.
|
||
|
func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, error)) {
|
||
|
p := m.newPart(ct)
|
||
|
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{}) 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)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// WriteTo writes the formatted 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)
|
||
|
return mw.n, mw.err
|
||
|
}
|
||
|
|
||
|
// 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 a 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)
|
||
|
}
|
||
|
if se == nil || si == nil {
|
||
|
return fmt.Errorf("received nil for STDERR or STDIN pipe")
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// SendError returns the sendError field of the Msg
|
||
|
func (m *Msg) SendError() error {
|
||
|
return m.sendError
|
||
|
}
|
||
|
|
||
|
// 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 {
|
||
|
c := 0
|
||
|
for _, p := range m.parts {
|
||
|
if !p.del {
|
||
|
c++
|
||
|
}
|
||
|
}
|
||
|
return c > 1
|
||
|
}
|
||
|
|
||
|
// newPart returns a new Part for the Msg
|
||
|
func (m *Msg) newPart(ct ContentType) *Part {
|
||
|
return &Part{
|
||
|
ctype: ct,
|
||
|
cset: m.charset,
|
||
|
enc: m.encoding,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// setEncoder creates a new mime.WordEncoder based on the encoding setting of the message
|
||
|
func (m *Msg) setEncoder() {
|
||
|
m.encoder = getEncoder(m.encoding)
|
||
|
}
|
||
|
|
||
|
// 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.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|