Files
RenewCMS/internal/infrastructure/mailer/message.go
T

91 lines
2.0 KiB
Go

package mailer
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"mime"
"mime/quotedprintable"
"net/mail"
"strings"
"time"
)
const (
senderDisplayName = "RenewCMS"
messageIDBytes = 16
lineBreak = "\r\n"
)
type message struct {
from mail.Address
to mail.Address
subject string
body string
sentAt time.Time
}
func newMessage(sender string, receiver string, subject string, body string, sentAt time.Time) (message, error) {
from, err := mail.ParseAddress(sender)
if err != nil {
return message{}, fmt.Errorf("invalid sender address: %w", err)
}
to, err := mail.ParseAddress(receiver)
if err != nil {
return message{}, fmt.Errorf("invalid receiver address: %w", err)
}
if strings.ContainsAny(subject, "\r\n") {
return message{}, errors.New("subject contains a line break")
}
return message{
from: mail.Address{Name: senderDisplayName, Address: from.Address},
to: *to,
subject: subject,
body: body,
sentAt: sentAt,
}, nil
}
func (m message) messageID() (string, error) {
_, domain, _ := strings.Cut(m.from.Address, "@")
random := make([]byte, messageIDBytes)
if _, err := rand.Read(random); err != nil {
return "", err
}
return "<" + hex.EncodeToString(random) + "@" + domain + ">", nil
}
func (m message) bytes() ([]byte, error) {
id, err := m.messageID()
if err != nil {
return nil, err
}
var out bytes.Buffer
headers := []struct{ name, value string }{
{"From", m.from.String()},
{"To", m.to.String()},
{"Subject", mime.QEncoding.Encode("utf-8", m.subject)},
{"Date", m.sentAt.Format(time.RFC1123Z)},
{"Message-ID", id},
{"MIME-Version", "1.0"},
{"Content-Type", "text/html; charset=UTF-8"},
{"Content-Transfer-Encoding", "quoted-printable"},
}
for _, h := range headers {
out.WriteString(h.name + ": " + h.value + lineBreak)
}
out.WriteString(lineBreak)
encoder := quotedprintable.NewWriter(&out)
if _, err := encoder.Write([]byte(m.body)); err != nil {
return nil, err
}
if err := encoder.Close(); err != nil {
return nil, err
}
return out.Bytes(), nil
}