//go:build unit package service import ( "bytes" "io" "mime" "mime/quotedprintable" "net/mail" "regexp" "strings" "testing" "github.com/stretchr/testify/require" ) func TestBuildSMTPMessageProducesStandardsCompliantMIME(t *testing.T) { config := &SMTPConfig{ Host: "smtp.example.com", From: "reply@example.com", FromName: "Sub2API 通知", } body := "\n验证码:123456 & ready\n" message, err := buildSMTPMessage(config, "User ", "邮箱验证码", body) require.NoError(t, err) require.Equal(t, "reply@example.com", message.envelopeFrom) require.Equal(t, "user@example.net", message.envelopeTo) parsed, err := mail.ReadMessage(bytes.NewReader(message.data)) require.NoError(t, err) from, err := mail.ParseAddress(parsed.Header.Get("From")) require.NoError(t, err) require.Equal(t, "Sub2API 通知", from.Name) require.Equal(t, "reply@example.com", from.Address) recipient, err := mail.ParseAddress(parsed.Header.Get("To")) require.NoError(t, err) require.Equal(t, "User", recipient.Name) require.Equal(t, "user@example.net", recipient.Address) decodedSubject, err := new(mime.WordDecoder).DecodeHeader(parsed.Header.Get("Subject")) require.NoError(t, err) require.Equal(t, "邮箱验证码", decodedSubject) require.NotEmpty(t, parsed.Header.Get("Date")) _, err = mail.ParseDate(parsed.Header.Get("Date")) require.NoError(t, err) require.Regexp(t, regexp.MustCompile(`^<[0-9a-f]{32}@example\.com>$`), parsed.Header.Get("Message-ID")) require.Equal(t, "1.0", parsed.Header.Get("MIME-Version")) require.Equal(t, "quoted-printable", parsed.Header.Get("Content-Transfer-Encoding")) mediaType, params, err := mime.ParseMediaType(parsed.Header.Get("Content-Type")) require.NoError(t, err) require.Equal(t, "text/html", mediaType) require.Equal(t, "UTF-8", params["charset"]) decodedBody, err := io.ReadAll(quotedprintable.NewReader(parsed.Body)) require.NoError(t, err) require.Equal(t, strings.ReplaceAll(body, "\n", "\r\n"), string(decodedBody)) } func TestBuildSMTPMessagePreventsHeaderInjection(t *testing.T) { config := &SMTPConfig{ Host: "smtp.example.com", From: "reply@example.com", FromName: "Sender\r\nBcc: hidden@example.com", } message, err := buildSMTPMessage(config, "user@example.net", "Subject\r\nCc: hidden@example.com", "body") require.NoError(t, err) parsed, err := mail.ReadMessage(bytes.NewReader(message.data)) require.NoError(t, err) require.Empty(t, parsed.Header.Get("Bcc")) require.Empty(t, parsed.Header.Get("Cc")) decodedSubject, err := new(mime.WordDecoder).DecodeHeader(parsed.Header.Get("Subject")) require.NoError(t, err) require.Equal(t, "SubjectCc: hidden@example.com", decodedSubject) } func TestBuildSMTPMessageRejectsInvalidConfiguration(t *testing.T) { _, err := buildSMTPMessage(nil, "user@example.net", "subject", "body") require.ErrorContains(t, err, "missing SMTP configuration") _, err = buildSMTPMessage(&SMTPConfig{Host: "smtp.example.com"}, "user@example.net", "subject", "body") require.ErrorContains(t, err, "invalid SMTP from address") _, err = buildSMTPMessage(&SMTPConfig{ Host: "smtp.example.com", From: "reply@example.com", }, "invalid recipient <>", "subject", "body") require.ErrorContains(t, err, "invalid SMTP recipient address") _, err = buildSMTPMessage(&SMTPConfig{ Host: "smtp.example.com", From: "reply@example.com", }, "user@example.net\r\nBcc: hidden@example.net", "subject", "body") require.ErrorContains(t, err, "invalid SMTP recipient address") } func TestBuildSMTPMessageUsesUniqueMessageIDs(t *testing.T) { config := &SMTPConfig{Host: "smtp.example.com", From: "reply@example.com"} first, err := buildSMTPMessage(config, "user@example.net", "subject", "body") require.NoError(t, err) second, err := buildSMTPMessage(config, "user@example.net", "subject", "body") require.NoError(t, err) firstParsed, err := mail.ReadMessage(bytes.NewReader(first.data)) require.NoError(t, err) secondParsed, err := mail.ReadMessage(bytes.NewReader(second.data)) require.NoError(t, err) require.NotEqual(t, firstParsed.Header.Get("Message-ID"), secondParsed.Header.Get("Message-ID")) }