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 }