email_message_test.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. //go:build unit
  2. package service
  3. import (
  4. "bytes"
  5. "io"
  6. "mime"
  7. "mime/quotedprintable"
  8. "net/mail"
  9. "regexp"
  10. "strings"
  11. "testing"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func TestBuildSMTPMessageProducesStandardsCompliantMIME(t *testing.T) {
  15. config := &SMTPConfig{
  16. Host: "smtp.example.com",
  17. From: "reply@example.com",
  18. FromName: "Sub2API 通知",
  19. }
  20. body := "<html>\n<body>验证码:123456 &amp; ready</body>\n</html>"
  21. message, err := buildSMTPMessage(config, "User <user@example.net>", "邮箱验证码", body)
  22. require.NoError(t, err)
  23. require.Equal(t, "reply@example.com", message.envelopeFrom)
  24. require.Equal(t, "user@example.net", message.envelopeTo)
  25. parsed, err := mail.ReadMessage(bytes.NewReader(message.data))
  26. require.NoError(t, err)
  27. from, err := mail.ParseAddress(parsed.Header.Get("From"))
  28. require.NoError(t, err)
  29. require.Equal(t, "Sub2API 通知", from.Name)
  30. require.Equal(t, "reply@example.com", from.Address)
  31. recipient, err := mail.ParseAddress(parsed.Header.Get("To"))
  32. require.NoError(t, err)
  33. require.Equal(t, "User", recipient.Name)
  34. require.Equal(t, "user@example.net", recipient.Address)
  35. decodedSubject, err := new(mime.WordDecoder).DecodeHeader(parsed.Header.Get("Subject"))
  36. require.NoError(t, err)
  37. require.Equal(t, "邮箱验证码", decodedSubject)
  38. require.NotEmpty(t, parsed.Header.Get("Date"))
  39. _, err = mail.ParseDate(parsed.Header.Get("Date"))
  40. require.NoError(t, err)
  41. require.Regexp(t, regexp.MustCompile(`^<[0-9a-f]{32}@example\.com>$`), parsed.Header.Get("Message-ID"))
  42. require.Equal(t, "1.0", parsed.Header.Get("MIME-Version"))
  43. require.Equal(t, "quoted-printable", parsed.Header.Get("Content-Transfer-Encoding"))
  44. mediaType, params, err := mime.ParseMediaType(parsed.Header.Get("Content-Type"))
  45. require.NoError(t, err)
  46. require.Equal(t, "text/html", mediaType)
  47. require.Equal(t, "UTF-8", params["charset"])
  48. decodedBody, err := io.ReadAll(quotedprintable.NewReader(parsed.Body))
  49. require.NoError(t, err)
  50. require.Equal(t, strings.ReplaceAll(body, "\n", "\r\n"), string(decodedBody))
  51. }
  52. func TestBuildSMTPMessagePreventsHeaderInjection(t *testing.T) {
  53. config := &SMTPConfig{
  54. Host: "smtp.example.com",
  55. From: "reply@example.com",
  56. FromName: "Sender\r\nBcc: hidden@example.com",
  57. }
  58. message, err := buildSMTPMessage(config, "user@example.net", "Subject\r\nCc: hidden@example.com", "body")
  59. require.NoError(t, err)
  60. parsed, err := mail.ReadMessage(bytes.NewReader(message.data))
  61. require.NoError(t, err)
  62. require.Empty(t, parsed.Header.Get("Bcc"))
  63. require.Empty(t, parsed.Header.Get("Cc"))
  64. decodedSubject, err := new(mime.WordDecoder).DecodeHeader(parsed.Header.Get("Subject"))
  65. require.NoError(t, err)
  66. require.Equal(t, "SubjectCc: hidden@example.com", decodedSubject)
  67. }
  68. func TestBuildSMTPMessageRejectsInvalidConfiguration(t *testing.T) {
  69. _, err := buildSMTPMessage(nil, "user@example.net", "subject", "body")
  70. require.ErrorContains(t, err, "missing SMTP configuration")
  71. _, err = buildSMTPMessage(&SMTPConfig{Host: "smtp.example.com"}, "user@example.net", "subject", "body")
  72. require.ErrorContains(t, err, "invalid SMTP from address")
  73. _, err = buildSMTPMessage(&SMTPConfig{
  74. Host: "smtp.example.com",
  75. From: "reply@example.com",
  76. }, "invalid recipient <>", "subject", "body")
  77. require.ErrorContains(t, err, "invalid SMTP recipient address")
  78. _, err = buildSMTPMessage(&SMTPConfig{
  79. Host: "smtp.example.com",
  80. From: "reply@example.com",
  81. }, "user@example.net\r\nBcc: hidden@example.net", "subject", "body")
  82. require.ErrorContains(t, err, "invalid SMTP recipient address")
  83. }
  84. func TestBuildSMTPMessageUsesUniqueMessageIDs(t *testing.T) {
  85. config := &SMTPConfig{Host: "smtp.example.com", From: "reply@example.com"}
  86. first, err := buildSMTPMessage(config, "user@example.net", "subject", "body")
  87. require.NoError(t, err)
  88. second, err := buildSMTPMessage(config, "user@example.net", "subject", "body")
  89. require.NoError(t, err)
  90. firstParsed, err := mail.ReadMessage(bytes.NewReader(first.data))
  91. require.NoError(t, err)
  92. secondParsed, err := mail.ReadMessage(bytes.NewReader(second.data))
  93. require.NoError(t, err)
  94. require.NotEqual(t, firstParsed.Header.Get("Message-ID"), secondParsed.Header.Get("Message-ID"))
  95. }