go-mail/msgwriter.go
Winni Neessen 19a3cf61ed
Refactor variable names for code readability
Updated variable names in multiple files to enhance code readability and maintainability by replacing abbreviations with full descriptive names. This ensures adherence to the best practices of naming conventions in Go.
2024-02-27 11:21:28 +01:00

380 lines
9.9 KiB
Go

// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/textproto"
"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 {
bytesWritten int64
charset Charset
depth int8
encoder mime.WordEncoder
err error
multiPartWriter [3]*multipart.Writer
partWriter io.Writer
writer io.Writer
}
// Write implements the io.Writer interface for msgWriter
func (mw *msgWriter) Write(payload []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.writer.Write(payload)
mw.bytesWritten += int64(n)
return n, mw.err
}
// writeMsg formats the message and sends it to its io.Writer
func (mw *msgWriter) writeMsg(msg *Msg) {
msg.addDefaultHeader()
msg.checkUserAgent()
mw.writeGenHeader(msg)
mw.writePreformattedGenHeader(msg)
// Set the FROM header (or envelope FROM if FROM is empty)
hasFrom := true
from, ok := msg.addrHeader[HeaderFrom]
if !ok || (len(from) == 0 || from == nil) {
from, ok = msg.addrHeader[HeaderEnvelopeFrom]
if !ok || (len(from) == 0 || from == nil) {
hasFrom = false
}
}
if hasFrom && (len(from) > 0 && from[0] != nil) {
mw.writeHeader(Header(HeaderFrom), from[0].String())
}
// Set the rest of the address headers
for _, to := range []AddrHeader{HeaderTo, HeaderCc} {
if addresses, ok := msg.addrHeader[to]; ok {
var val []string
for _, addr := range addresses {
val = append(val, addr.String())
}
mw.writeHeader(Header(to), val...)
}
}
if msg.hasMixed() {
mw.startMP("mixed", msg.boundary)
mw.writeString(DoubleNewLine)
}
if msg.hasRelated() {
mw.startMP("related", msg.boundary)
mw.writeString(DoubleNewLine)
}
if msg.hasAlt() {
mw.startMP(MIMEAlternative, msg.boundary)
mw.writeString(DoubleNewLine)
}
if msg.hasPGPType() {
switch msg.pgptype {
case PGPEncrypt:
mw.startMP(`encrypted; protocol="application/pgp-encrypted"`,
msg.boundary)
case PGPSignature:
mw.startMP(`signed; protocol="application/pgp-signature";`,
msg.boundary)
default:
}
mw.writeString(DoubleNewLine)
}
for _, part := range msg.parts {
if !part.isDeleted {
mw.writePart(part, msg.charset)
}
}
if msg.hasAlt() {
mw.stopMP()
}
// Add embeds
mw.addFiles(msg.embeds, false)
if msg.hasRelated() {
mw.stopMP()
}
// Add attachments
mw.addFiles(msg.attachments, true)
if msg.hasMixed() {
mw.stopMP()
}
}
// writeGenHeader writes out all generic headers to the msgWriter
func (mw *msgWriter) writeGenHeader(msg *Msg) {
keys := make([]string, 0, len(msg.genHeader))
for key := range msg.genHeader {
keys = append(keys, string(key))
}
sort.Strings(keys)
for _, key := range keys {
mw.writeHeader(Header(key), msg.genHeader[Header(key)]...)
}
}
// writePreformatedHeader writes out all preformated generic headers to the msgWriter
func (mw *msgWriter) writePreformattedGenHeader(msg *Msg) {
for key, val := range msg.preformHeader {
mw.writeString(fmt.Sprintf("%s: %s%s", key, val, SingleNewLine))
}
}
// startMP writes a multipart beginning
func (mw *msgWriter) startMP(mimeType MIMEType, boundary string) {
multiPartWriter := multipart.NewWriter(mw)
if boundary != "" {
mw.err = multiPartWriter.SetBoundary(boundary)
}
contentType := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mimeType,
multiPartWriter.Boundary())
mw.multiPartWriter[mw.depth] = multiPartWriter
if mw.depth == 0 {
mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, contentType))
}
if mw.depth > 0 {
mw.newPart(map[string][]string{"Content-Type": {contentType}})
}
mw.depth++
}
// stopMP closes the multipart
func (mw *msgWriter) stopMP() {
if mw.depth > 0 {
mw.err = mw.multiPartWriter[mw.depth-1].Close()
mw.depth--
}
}
// addFiles adds the attachments/embeds file content to the mail body
func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
for _, file := range files {
encoding := EncodingB64
if _, ok := file.getHeader(HeaderContentType); !ok {
mimeType := mime.TypeByExtension(filepath.Ext(file.Name))
if mimeType == "" {
mimeType = "application/octet-stream"
}
if file.ContentType != "" {
mimeType = string(file.ContentType)
}
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
mw.encoder.Encode(mw.charset.String(), file.Name)))
}
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
if file.Enc != "" {
encoding = file.Enc
}
file.setHeader(HeaderContentTransferEnc, string(encoding))
}
if file.Desc != "" {
if _, ok := file.getHeader(HeaderContentDescription); !ok {
file.setHeader(HeaderContentDescription, file.Desc)
}
}
if _, ok := file.getHeader(HeaderContentDisposition); !ok {
disposition := "inline"
if isAttachment {
disposition = "attachment"
}
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
disposition, mw.encoder.Encode(mw.charset.String(), file.Name)))
}
if !isAttachment {
if _, ok := file.getHeader(HeaderContentID); !ok {
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", file.Name))
}
}
if mw.depth == 0 {
for header, val := range file.Header {
mw.writeHeader(Header(header), val...)
}
mw.writeString(SingleNewLine)
}
if mw.depth > 0 {
mw.newPart(file.Header)
}
if mw.err == nil {
mw.writeBody(file.Writer, encoding)
}
}
}
// newPart creates a new MIME multipart io.Writer and sets the partwriter to it
func (mw *msgWriter) newPart(header map[string][]string) {
mw.partWriter, mw.err = mw.multiPartWriter[mw.depth-1].CreatePart(header)
}
// writePart writes the corresponding part to the Msg body
func (mw *msgWriter) writePart(part *Part, charset Charset) {
partCharset := part.charset
if partCharset.String() == "" {
partCharset = charset
}
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset)
contentTransferEnc := part.encoding.String()
if mw.depth == 0 {
mw.writeHeader(HeaderContentType, contentType)
mw.writeHeader(HeaderContentTransferEnc, contentTransferEnc)
mw.writeString(SingleNewLine)
}
if mw.depth > 0 {
mimeHeader := textproto.MIMEHeader{}
if part.description != "" {
mimeHeader.Add(string(HeaderContentDescription), part.description)
}
mimeHeader.Add(string(HeaderContentType), contentType)
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
mw.newPart(mimeHeader)
}
mw.writeBody(part.writeFunc, part.encoding)
}
// 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.writer, s)
mw.bytesWritten += int64(n)
}
// writeHeader writes a header into the msgWriter's io.Writer
func (mw *msgWriter) writeHeader(key Header, values ...string) {
buffer := strings.Builder{}
charLength := MaxHeaderLength - 2
buffer.WriteString(string(key))
charLength -= len(key)
if len(values) == 0 {
buffer.WriteString(":\r\n")
return
}
buffer.WriteString(": ")
charLength -= 2
fullValueStr := strings.Join(values, ", ")
words := strings.Split(fullValueStr, " ")
for i, val := range words {
if charLength-len(val) <= 1 {
buffer.WriteString(fmt.Sprintf("%s ", SingleNewLine))
charLength = MaxHeaderLength - 3
}
buffer.WriteString(val)
if i < len(words)-1 {
buffer.WriteString(" ")
charLength -= 1
}
charLength -= len(val)
}
bufferString := buffer.String()
bufferString = strings.ReplaceAll(bufferString, fmt.Sprintf(" %s", SingleNewLine),
SingleNewLine)
mw.writeString(bufferString)
mw.writeString("\r\n")
}
// writeBody writes an io.Reader into an io.Writer using provided Encoding
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
var writer io.Writer
var encodedWriter io.WriteCloser
var n int64
var err error
if mw.depth == 0 {
writer = mw.writer
}
if mw.depth > 0 {
writer = mw.partWriter
}
writeBuffer := bytes.Buffer{}
lineBreaker := Base64LineBreaker{}
lineBreaker.out = &writeBuffer
switch encoding {
case EncodingQP:
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
case EncodingB64:
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
case NoEncoding:
_, err = writeFunc(&writeBuffer)
if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err)
}
n, err = io.Copy(writer, &writeBuffer)
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
}
if mw.depth == 0 {
mw.bytesWritten += n
}
return
default:
encodedWriter = quotedprintable.NewWriter(writer)
}
_, err = writeFunc(encodedWriter)
if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err)
}
err = encodedWriter.Close()
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err)
}
err = lineBreaker.Close()
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err)
}
n, err = io.Copy(writer, &writeBuffer)
if err != nil && mw.err == nil {
mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err)
}
// Since the part writer uses the WriteTo() method, we don't need to add the
// bytes twice
if mw.depth == 0 {
mw.bytesWritten += n
}
}