1
0
Fork 0
golang-github-domodwyer-mai.../mime.go
Daniel Baumann cb9cbb7a25
Adding upstream version 3.6.2.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 21:13:13 +02:00

202 lines
5 KiB
Go

package mailyak
import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"mime/quotedprintable"
"net/textproto"
"strings"
)
func (m *MailYak) buildMime(w io.Writer) error {
mb, err := randomBoundary()
if err != nil {
return err
}
ab, err := randomBoundary()
if err != nil {
return err
}
return m.buildMimeWithBoundaries(w, mb, ab)
}
// randomBoundary returns a random hexadecimal string used for separating MIME
// parts.
//
// The returned string must be sufficiently random to prevent malicious users
// from performing content injection attacks.
func randomBoundary() (string, error) {
buf := make([]byte, 30)
_, err := io.ReadFull(rand.Reader, buf)
if err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
// buildMimeWithBoundaries creates the MIME message using mb and ab as MIME
// boundaries, and returns the generated MIME data as a buffer.
func (m *MailYak) buildMimeWithBoundaries(w io.Writer, mb, ab string) error {
if err := m.writeHeaders(w); err != nil {
return err
}
var (
hasBody = m.html.Len() != 0 || m.plain.Len() != 0
hasAttachments = len(m.attachments) != 0
)
// if we don't have text/html body or attachments we can skip Content-Type header
// in that case next default will be assumed
// Content-type: text/plain; charset=us-ascii
// See https://datatracker.ietf.org/doc/html/rfc2045#page-14
if !hasBody && !hasAttachments {
// The RFC said that body can be ommited: https://datatracker.ietf.org/doc/html/rfc822#section-3.1
// but for example mailhog can't correctly read message without any body.
// This CRLF just separate header section.
_, err := fmt.Fprint(w, "\r\n")
return err
}
// Start our multipart/mixed part
mixed := multipart.NewWriter(w)
if err := mixed.SetBoundary(mb); err != nil {
return err
}
// To avoid deferring a mixed.Close(), run the write in a closure and
// close the mixed after.
tryWrite := func() error {
fmt.Fprintf(w, "Content-Type: multipart/mixed;\r\n\tboundary=\"%s\"; charset=UTF-8\r\n\r\n", mixed.Boundary())
if hasBody {
if err := m.writeAlternativePart(mixed, ab); err != nil {
return err
}
}
if hasAttachments {
if err := m.writeAttachments(mixed, lineSplitterBuilder{}); err != nil {
return err
}
}
return nil
}
if err := tryWrite(); err != nil {
return err
}
return mixed.Close()
}
// writeHeaders writes the MIME-Version, Date, Reply-To, From, To and Subject headers,
// plus any custom headers set via AddHeader().
func (m *MailYak) writeHeaders(w io.Writer) error {
if _, err := w.Write([]byte(m.fromHeader())); err != nil {
return err
}
if _, err := w.Write([]byte("MIME-Version: 1.0\r\n")); err != nil {
return err
}
fmt.Fprintf(w, "Date: %s\r\n", m.date)
if m.replyTo != "" {
fmt.Fprintf(w, "Reply-To: %s\r\n", m.replyTo)
}
fmt.Fprintf(w, "Subject: %s\r\n", m.subject)
if len(m.toAddrs) > 0 {
commaSeparatedToAddrs := strings.Join(m.toAddrs, ",")
fmt.Fprintf(w, "To: %s\r\n", commaSeparatedToAddrs)
}
if len(m.ccAddrs) > 0 {
commaSeparatedCCAddrs := strings.Join(m.ccAddrs, ",")
fmt.Fprintf(w, "CC: %s\r\n", commaSeparatedCCAddrs)
}
if m.writeBccHeader && len(m.bccAddrs) > 0 {
commaSeparatedBCCAddrs := strings.Join(m.bccAddrs, ",")
fmt.Fprintf(w, "BCC: %s\r\n", commaSeparatedBCCAddrs)
}
for k, values := range m.headers {
for _, v := range values {
fmt.Fprintf(w, "%s: %s\r\n", k, v)
}
}
return nil
}
// fromHeader returns a correctly formatted From header, optionally with a name
// component.
func (m *MailYak) fromHeader() string {
if m.fromName == "" {
return fmt.Sprintf("From: %s\r\n", m.fromAddr)
}
return fmt.Sprintf("From: %s <%s>\r\n", m.fromName, m.fromAddr)
}
func (m *MailYak) writeAlternativePart(mixed *multipart.Writer, boundary string) error {
ctype := fmt.Sprintf("multipart/alternative;\r\n\tboundary=\"%s\"", boundary)
altPart, err := mixed.CreatePart(textproto.MIMEHeader{"Content-Type": {ctype}})
if err != nil {
return err
}
return m.writeBody(altPart, boundary)
}
// writeBody writes the text/plain and text/html mime parts.
// It's incorrect to call writeBody without html or plain content.
func (m *MailYak) writeBody(w io.Writer, boundary string) error {
alt := multipart.NewWriter(w)
if err := alt.SetBoundary(boundary); err != nil {
return err
}
var err error
writePart := func(ctype string, data []byte) {
if len(data) == 0 || err != nil {
return
}
c := fmt.Sprintf("%s; charset=UTF-8", ctype)
var part io.Writer
part, err = alt.CreatePart(textproto.MIMEHeader{"Content-Type": {c}, "Content-Transfer-Encoding": {"quoted-printable"}})
if err != nil {
return
}
var buf bytes.Buffer
qpw := quotedprintable.NewWriter(&buf)
_, _ = qpw.Write(data)
_ = qpw.Close()
_, err = part.Write(buf.Bytes())
}
writePart("text/plain", m.plain.Bytes())
writePart("text/html", m.html.Bytes())
// If closing the alt fails, and there's not already an error set, return the
// close error.
if closeErr := alt.Close(); err == nil && closeErr != nil {
return closeErr
}
return err
}