go-mail/msgwriter.go

314 lines
7 KiB
Go
Raw Permalink Normal View History

// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"encoding/base64"
"fmt"
"io"
2022-03-14 10:29:53 +01:00
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/textproto"
2022-03-14 10:29:53 +01:00
"path/filepath"
"sort"
"strings"
)
// MaxHeaderLength defines the maximum line length for a mail header
// RFC 2047 suggests 76 characters
const MaxHeaderLength = 76
// MaxBodyLength defines the maximum line length for the mail body
// RFC 2047 suggests 76 characters
const MaxBodyLength = 76
// SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return
const SingleNewLine = "\r\n"
// DoubleNewLine represents a double new line that can be used by the msgWriter to
// indicate a new segement of the mail
const DoubleNewLine = "\r\n\r\n"
// msgWriter handles the I/O to the io.WriteCloser of the SMTP client
type msgWriter struct {
c Charset
d int8
en mime.WordEncoder
err error
mpw [3]*multipart.Writer
n int64
pw io.Writer
w io.Writer
}
// Write implements the io.Writer interface for msgWriter
func (mw *msgWriter) Write(p []byte) (int, error) {
if mw.err != nil {
return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err)
}
var n int
n, mw.err = mw.w.Write(p)
mw.n += int64(n)
return n, mw.err
}
// writeMsg formats the message and sends it to its io.Writer
func (mw *msgWriter) writeMsg(m *Msg) {
m.addDefaultHeader()
m.checkUserAgent()
mw.writeGenHeader(m)
// Set the FROM header (or envelope FROM if FROM is empty)
hf := true
f, ok := m.addrHeader[HeaderFrom]
if !ok || len(f) == 0 {
f, ok = m.addrHeader[HeaderEnvelopeFrom]
if !ok || len(f) == 0 {
hf = false
}
}
if hf {
mw.writeHeader(Header(HeaderFrom), f[0].String())
}
// Set the rest of the address headers
for _, t := range []AddrHeader{HeaderTo, HeaderCc} {
if al, ok := m.addrHeader[t]; ok {
var v []string
for _, a := range al {
v = append(v, a.String())
}
mw.writeHeader(Header(t), v...)
}
}
2022-03-14 10:29:53 +01:00
if m.hasMixed() {
mw.startMP("mixed", m.boundary)
mw.writeString(DoubleNewLine)
2022-03-14 10:29:53 +01:00
}
if m.hasRelated() {
mw.startMP("related", m.boundary)
mw.writeString(DoubleNewLine)
2022-03-14 10:29:53 +01:00
}
if m.hasAlt() {
mw.startMP(MIMEAlternative, m.boundary)
mw.writeString(DoubleNewLine)
}
2022-03-14 10:29:53 +01:00
for _, p := range m.parts {
mw.writePart(p, m.charset)
}
if m.hasAlt() {
mw.stopMP()
}
2022-03-14 10:29:53 +01:00
// Add embeds
mw.addFiles(m.embeds, false)
if m.hasRelated() {
mw.stopMP()
}
// Add attachments
mw.addFiles(m.attachments, true)
if m.hasMixed() {
mw.stopMP()
}
}
// writeGenHeader writes out all generic headers to the msgWriter
func (mw *msgWriter) writeGenHeader(m *Msg) {
gk := make([]string, 0, len(m.genHeader))
for k := range m.genHeader {
gk = append(gk, string(k))
}
sort.Strings(gk)
for _, k := range gk {
mw.writeHeader(Header(k), m.genHeader[Header(k)]...)
}
}
// startMP writes a multipart beginning
func (mw *msgWriter) startMP(mt MIMEType, b string) {
mp := multipart.NewWriter(mw)
if b != "" {
mw.err = mp.SetBoundary(b)
}
ct := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mt, mp.Boundary())
mw.mpw[mw.d] = mp
if mw.d == 0 {
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, ct))
}
if mw.d > 0 {
mw.newPart(map[string][]string{"Content-Type": {ct}})
}
mw.d++
}
// stopMP closes the multipart
func (mw *msgWriter) stopMP() {
if mw.d > 0 {
mw.err = mw.mpw[mw.d-1].Close()
mw.d--
}
}
2022-03-14 10:29:53 +01:00
// addFiles adds the attachments/embeds file content to the mail body
func (mw *msgWriter) addFiles(fl []*File, a bool) {
for _, f := range fl {
if _, ok := f.getHeader(HeaderContentType); !ok {
mt := mime.TypeByExtension(filepath.Ext(f.Name))
if mt == "" {
mt = "application/octet-stream"
}
f.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mt,
mw.en.Encode(mw.c.String(), f.Name)))
2022-03-14 10:29:53 +01:00
}
if _, ok := f.getHeader(HeaderContentTransferEnc); !ok {
f.setHeader(HeaderContentTransferEnc, string(EncodingB64))
}
if _, ok := f.getHeader(HeaderContentDisposition); !ok {
d := "inline"
if a {
d = "attachment"
}
f.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, d,
mw.en.Encode(mw.c.String(), f.Name)))
2022-03-14 10:29:53 +01:00
}
if !a {
if _, ok := f.getHeader(HeaderContentID); !ok {
f.setHeader(HeaderContentID, fmt.Sprintf("<%s>", f.Name))
}
}
if mw.d == 0 {
for h, v := range f.Header {
mw.writeHeader(Header(h), v...)
}
mw.writeString(SingleNewLine)
2022-03-14 10:29:53 +01:00
}
if mw.d > 0 {
mw.newPart(f.Header)
}
mw.writeBody(f.Writer, EncodingB64)
}
}
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it
func (mw *msgWriter) newPart(h map[string][]string) {
mw.pw, mw.err = mw.mpw[mw.d-1].CreatePart(h)
}
// writePart writes the corresponding part to the Msg body
func (mw *msgWriter) writePart(p *Part, cs Charset) {
2022-03-13 19:04:58 +01:00
ct := fmt.Sprintf("%s; charset=%s", p.ctype, cs)
2022-03-13 20:04:30 +01:00
cte := p.enc.String()
2022-03-13 19:04:58 +01:00
if mw.d == 0 {
mw.writeHeader(HeaderContentType, ct)
mw.writeHeader(HeaderContentTransferEnc, cte)
mw.writeString(SingleNewLine)
2022-03-13 19:04:58 +01:00
}
if mw.d > 0 {
2022-03-13 19:04:58 +01:00
mh := textproto.MIMEHeader{}
mh.Add(string(HeaderContentType), ct)
mh.Add(string(HeaderContentTransferEnc), cte)
mw.newPart(mh)
}
mw.writeBody(p.w, p.enc)
}
// writeString writes a string into the msgWriter's io.Writer interface
func (mw *msgWriter) writeString(s string) {
if mw.err != nil {
return
}
var n int
n, mw.err = io.WriteString(mw.w, s)
mw.n += int64(n)
}
// writeHeader writes a header into the msgWriter's io.Writer
func (mw *msgWriter) writeHeader(k Header, vl ...string) {
wbuf := bytes.Buffer{}
cl := MaxHeaderLength - 2
wbuf.WriteString(string(k))
cl -= len(k)
if len(vl) == 0 {
wbuf.WriteString(":\r\n")
return
}
wbuf.WriteString(": ")
cl -= 2
fs := strings.Join(vl, ", ")
sfs := strings.Split(fs, " ")
for i, v := range sfs {
if cl-len(v) <= 1 {
wbuf.WriteString(fmt.Sprintf("%s ", SingleNewLine))
cl = MaxHeaderLength - 3
}
wbuf.WriteString(v)
if i < len(sfs)-1 {
wbuf.WriteString(" ")
cl -= 1
}
cl -= len(v)
}
bufs := wbuf.String()
bufs = strings.ReplaceAll(bufs, fmt.Sprintf(" %s", SingleNewLine), SingleNewLine)
mw.writeString(bufs)
mw.writeString("\r\n")
}
// writeBody writes an io.Reader into an io.Writer using provided Encoding
func (mw *msgWriter) writeBody(f func(io.Writer) (int64, error), e Encoding) {
var w io.Writer
var ew io.WriteCloser
var n int64
if mw.d == 0 {
w = mw.w
}
if mw.d > 0 {
w = mw.pw
}
wbuf := bytes.Buffer{}
lb := Base64LineBreaker{}
lb.out = &wbuf
switch e {
case EncodingQP:
ew = quotedprintable.NewWriter(&wbuf)
case EncodingB64:
ew = base64.NewEncoder(base64.StdEncoding, &lb)
case NoEncoding:
_, mw.err = f(&wbuf)
n, mw.err = io.Copy(w, &wbuf)
if mw.d == 0 {
mw.n += n
}
return
default:
ew = quotedprintable.NewWriter(w)
}
_, mw.err = f(ew)
mw.err = ew.Close()
mw.err = lb.Close()
n, mw.err = io.Copy(w, &wbuf)
// Since the part writer uses the WriteTo() method, we don't need to add the
// bytes twice
if mw.d == 0 {
mw.n += n
}
}