email_message.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. package service
  2. import (
  3. "bytes"
  4. "crypto/rand"
  5. "encoding/hex"
  6. "errors"
  7. "fmt"
  8. "mime"
  9. "mime/quotedprintable"
  10. "net/mail"
  11. "strings"
  12. "time"
  13. )
  14. type smtpMessage struct {
  15. envelopeFrom string
  16. envelopeTo string
  17. data []byte
  18. }
  19. func buildSMTPMessage(config *SMTPConfig, to, subject, body string) (smtpMessage, error) {
  20. if config == nil {
  21. return smtpMessage{}, errors.New("missing SMTP configuration")
  22. }
  23. fromAddress, err := parseSMTPAddress(config.From, "from")
  24. if err != nil {
  25. return smtpMessage{}, err
  26. }
  27. recipientAddress, err := parseSMTPAddress(to, "recipient")
  28. if err != nil {
  29. return smtpMessage{}, err
  30. }
  31. messageID, err := generateEmailMessageID(fromAddress.Address, config.Host)
  32. if err != nil {
  33. return smtpMessage{}, fmt.Errorf("generate message ID: %w", err)
  34. }
  35. fromName := sanitizeEmailHeader(config.FromName)
  36. if strings.TrimSpace(fromName) == "" {
  37. fromName = fromAddress.Name
  38. }
  39. fromHeader := (&mail.Address{
  40. Name: fromName,
  41. Address: fromAddress.Address,
  42. }).String()
  43. toHeader := (&mail.Address{
  44. Name: recipientAddress.Name,
  45. Address: recipientAddress.Address,
  46. }).String()
  47. subjectHeader := mime.QEncoding.Encode("UTF-8", sanitizeEmailHeader(subject))
  48. var message bytes.Buffer
  49. fmt.Fprintf(&message, "From: %s\r\n", fromHeader)
  50. fmt.Fprintf(&message, "To: %s\r\n", toHeader)
  51. fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
  52. fmt.Fprintf(&message, "Message-ID: %s\r\n", messageID)
  53. fmt.Fprintf(&message, "Subject: %s\r\n", subjectHeader)
  54. fmt.Fprint(&message, "MIME-Version: 1.0\r\n"+
  55. "Content-Type: text/html; charset=UTF-8\r\n"+
  56. "Content-Transfer-Encoding: quoted-printable\r\n\r\n")
  57. bodyWriter := quotedprintable.NewWriter(&message)
  58. if _, err := bodyWriter.Write([]byte(body)); err != nil {
  59. return smtpMessage{}, fmt.Errorf("encode email body: %w", err)
  60. }
  61. if err := bodyWriter.Close(); err != nil {
  62. return smtpMessage{}, fmt.Errorf("close email body encoder: %w", err)
  63. }
  64. return smtpMessage{
  65. envelopeFrom: fromAddress.Address,
  66. envelopeTo: recipientAddress.Address,
  67. data: message.Bytes(),
  68. }, nil
  69. }
  70. func parseSMTPAddress(value, field string) (*mail.Address, error) {
  71. if strings.ContainsAny(value, "\r\n") {
  72. return nil, fmt.Errorf("invalid SMTP %s address: contains a line break", field)
  73. }
  74. cleaned := strings.TrimSpace(value)
  75. address, err := mail.ParseAddress(cleaned)
  76. if err != nil || strings.TrimSpace(address.Address) == "" {
  77. if err == nil {
  78. err = fmt.Errorf("address is empty")
  79. }
  80. return nil, fmt.Errorf("invalid SMTP %s address: %w", field, err)
  81. }
  82. return address, nil
  83. }
  84. func generateEmailMessageID(fromAddress, smtpHost string) (string, error) {
  85. randomID := make([]byte, 16)
  86. if _, err := rand.Read(randomID); err != nil {
  87. return "", err
  88. }
  89. domain := strings.TrimSpace(sanitizeEmailHeader(smtpHost))
  90. if at := strings.LastIndexByte(fromAddress, '@'); at >= 0 && at < len(fromAddress)-1 {
  91. domain = fromAddress[at+1:]
  92. }
  93. domain = strings.Trim(domain, "[]<>")
  94. if domain == "" {
  95. domain = "localhost"
  96. }
  97. return fmt.Sprintf("<%s@%s>", hex.EncodeToString(randomID), domain), nil
  98. }