camper/pkg/mail/msg.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
}