// 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 }