Files
mailx/smtp/smtp_internal_test.go
T
2026-08-15 01:38:05 +08:00

331 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package smtp
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
mailx "code.yun.ink/pkg/mailx"
)
func TestBuildMIME(t *testing.T) {
s := New(Config{Host: "h", Port: 25, From: "default@example.com", ReplyTo: "dr@example.com"})
msg := mailx.NewMessage().
From("sender@example.com").
To("to@example.com").
Cc("cc@example.com").
Subject("主题").
Body("<p>hi</p>").
ReplyTo("msg-reply@example.com").
AttachBytes("a.txt", []byte("content")).
Build()
data, err := s.buildMIME(msg, "sender@example.com")
if err != nil {
t.Fatal(err)
}
out := string(data)
for _, want := range []string{
"From: sender@example.com",
"To: to@example.com",
"Cc: cc@example.com",
"Reply-To: msg-reply@example.com",
"MIME-Version: 1.0",
"multipart/mixed",
"text/html; charset=UTF-8",
"attachment; filename",
"Y29udGVudA==", // base64("content")
} {
if !strings.Contains(out, want) {
t.Errorf("MIME missing %q, got:\n%s", want, out)
}
}
}
func TestBuildMIMEWithoutBody(t *testing.T) {
s := New(Config{})
msg := mailx.NewMessage().
To("to@example.com").
Subject("s").
AttachBytes("a.txt", []byte("x")).
Build()
data, err := s.buildMIME(msg, "f@example.com")
if err != nil {
t.Fatal(err)
}
out := string(data)
if strings.Contains(out, "text/html") {
t.Errorf("should not contain html part, got:\n%s", out)
}
if !strings.Contains(out, "multipart/mixed") {
t.Errorf("should contain multipart/mixed for attachment, got:\n%s", out)
}
}
func TestBuildMIMEFromFallback(t *testing.T) {
// Message.From 为空时回退到 Config.From
s := New(Config{From: "cfg-from@example.com"})
msg := mailx.NewMessage().To("t@e.com").Subject("s").Build()
data, err := s.buildMIME(msg, "cfg-from@example.com")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "From: cfg-from@example.com") {
t.Fatalf("From header missing, got:\n%s", data)
}
}
func TestReadAttachment(t *testing.T) {
s := New(Config{})
// 按路径
dir := t.TempDir()
p := filepath.Join(dir, "x.txt")
if err := os.WriteFile(p, []byte("abc"), 0o600); err != nil {
t.Fatal(err)
}
name, data, err := s.readAttachment(mailx.Attachment{Path: p})
if err != nil {
t.Fatal(err)
}
if name != "x.txt" || string(data) != "abc" {
t.Errorf("name=%q data=%q", name, data)
}
// 内存字节
name, data, err = s.readAttachment(mailx.Attachment{Name: "m.bin", Data: []byte{1, 2}})
if err != nil {
t.Fatal(err)
}
if name != "m.bin" || !bytes.Equal(data, []byte{1, 2}) {
t.Errorf("name=%q data=%v", name, data)
}
// 空附件报错
if _, _, err := s.readAttachment(mailx.Attachment{}); err == nil {
t.Fatal("empty attachment should error")
}
// 路径不存在报错
if _, _, err := s.readAttachment(mailx.Attachment{Path: filepath.Join(dir, "nope.txt")}); err == nil {
t.Fatal("missing file should error")
}
}
func TestEncodeHeader(t *testing.T) {
if got := encodeHeader("plain"); got != "plain" {
t.Errorf("encodeHeader(plain) = %q", got)
}
got := encodeHeader("主题")
if !strings.HasPrefix(got, "=?UTF-8?q?") || !strings.HasSuffix(got, "?=") {
t.Errorf("encodeHeader(主题) = %q, want RFC 2047 encoded", got)
}
}
func TestEffectiveEncryption(t *testing.T) {
cases := []struct {
name string
cfg Config
want Encryption
}{
{"default 587 -> tls", Config{Host: "h", Port: 587}, EncryptionTLS},
{"default 465 -> ssl", Config{Host: "h", Port: 465}, EncryptionSSL},
{"default 25 -> tls", Config{Host: "h", Port: 25}, EncryptionTLS},
{"explicit ssl", Config{Host: "h", Port: 587, Encryption: EncryptionSSL}, EncryptionSSL},
{"explicit tls", Config{Host: "h", Port: 465, Encryption: EncryptionTLS}, EncryptionTLS},
{"explicit none", Config{Host: "h", Port: 465, Encryption: EncryptionNone}, EncryptionNone},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := New(c.cfg).effectiveEncryption(); got != c.want {
t.Errorf("effectiveEncryption() = %q, want %q", got, c.want)
}
})
}
}
func TestBuildMIMEInlineImage(t *testing.T) {
s := New(Config{})
msg := mailx.NewMessage().
From(`"张三" <sender@example.com>`).
To("to@example.com").
Subject("s").
HTML(`<p>hi <img src="cid:logo1"></p>`).
InlineImageBytes("logo1", "logo.png", []byte{0x89, 0x50, 0x4e, 0x47}).
Build()
data, err := s.buildMIME(msg, `"张三" <sender@example.com>`)
if err != nil {
t.Fatal(err)
}
out := string(data)
// 显示名应被 RFC2047 编码
if !strings.Contains(out, "From: =?UTF-8?q?") {
t.Errorf("display name not encoded, got:\n%s", out)
}
// related 结构 + 内嵌图片
if !strings.Contains(out, "multipart/related") {
t.Errorf("missing multipart/related, got:\n%s", out)
}
// textproto 会将 Content-ID 规范化为 Content-Id(RFC 允许,客户端能正确解析)
if !strings.Contains(out, "Content-Id: <logo1>") && !strings.Contains(out, "Content-ID: <logo1>") {
t.Errorf("missing Content-ID, got:\n%s", out)
}
if !strings.Contains(out, "inline; filename") {
t.Errorf("missing inline disposition, got:\n%s", out)
}
// png 签名 base640x89 0x50 0x4e 0x47 -> iVBORw==
if !strings.Contains(out, "iVBORw==") {
t.Errorf("png data missing, got:\n%s", out)
}
}
func TestBuildMIMECustomHeaders(t *testing.T) {
s := New(Config{})
msg := mailx.NewMessage().
To("t@e.com").
Subject("s").
Header("List-Unsubscribe", "<https://example.com/unsub>"). // 自定义头
Header("X-Mailer", "mailx").
Header("Subject", "should-not-override"). // 标准头,不应覆盖
Build()
data, err := s.buildMIME(msg, "f@e.com")
if err != nil {
t.Fatal(err)
}
out := string(data)
if !strings.Contains(out, "List-Unsubscribe: <https://example.com/unsub>") {
t.Errorf("missing custom header List-Unsubscribe, got:\n%s", out)
}
if !strings.Contains(out, "X-Mailer: mailx") {
t.Errorf("missing custom header X-Mailer, got:\n%s", out)
}
// 标准头 Subject 不应被自定义值覆盖
if strings.Contains(out, "Subject: should-not-override") {
t.Errorf("custom header overrode standard Subject, got:\n%s", out)
}
if !strings.Contains(out, "Subject: s") {
t.Errorf("standard Subject lost, got:\n%s", out)
}
}
func TestReadInline(t *testing.T) {
s := New(Config{})
// 按路径读取,MIME 类型由扩展名推断
dir := t.TempDir()
p := filepath.Join(dir, "img.png")
if err := os.WriteFile(p, []byte{0x89, 0x50, 0x4e, 0x47}, 0o600); err != nil {
t.Fatal(err)
}
name, data, mtype, err := s.readInline(mailx.InlineImage{CID: "cid1", Path: p})
if err != nil {
t.Fatal(err)
}
if name != "img.png" || string(data) != string([]byte{0x89, 0x50, 0x4e, 0x47}) {
t.Errorf("path inline: name=%q data=%v", name, data)
}
if mtype != "image/png" {
t.Errorf("mtype = %q, want image/png", mtype)
}
// 内存字节 + 显式 MIME 类型
name, data, mtype, err = s.readInline(mailx.InlineImage{CID: "c2", Name: "x.jpg", Data: []byte{1}, MIMEType: "image/jpeg"})
if err != nil {
t.Fatal(err)
}
if name != "x.jpg" || mtype != "image/jpeg" {
t.Errorf("memory inline: name=%q mtype=%q", name, mtype)
}
// 空 inline 报错
if _, _, _, err := s.readInline(mailx.InlineImage{}); err == nil {
t.Fatal("empty inline should error")
}
// 路径不存在报错
if _, _, _, err := s.readInline(mailx.InlineImage{CID: "c", Path: filepath.Join(dir, "nope.png")}); err == nil {
t.Fatal("missing inline file should error")
}
}
func TestFormatAddressHeader(t *testing.T) {
// 纯地址原样
if got := formatAddressHeader("a@example.com"); got != "a@example.com" {
t.Errorf("plain = %q", got)
}
// 带显示名
got := formatAddressHeader(`"张三" <a@example.com>`)
if !strings.Contains(got, "=?UTF-8?q?") || !strings.Contains(got, "<a@example.com>") {
t.Errorf("display name = %q", got)
}
// 非法地址回退为 RFC2047 编码
if got := formatAddressHeader("not-an-email"); got == "" {
t.Errorf("invalid address should fallback, got empty")
}
// 空字符串
if got := formatAddressHeader(""); got != "" {
t.Errorf("empty = %q, want empty", got)
}
}
func TestFormatAddressList(t *testing.T) {
got := formatAddressList([]string{"a@e.com", `"B" <b@e.com>`})
if !strings.Contains(got, "a@e.com") || !strings.Contains(got, "b@e.com") {
t.Errorf("list = %q", got)
}
// 空列表
if got := formatAddressList(nil); got != "" {
t.Errorf("empty list = %q", got)
}
}
func TestBaseNameAndPathExt(t *testing.T) {
if got := baseName("/a/b/c.txt"); got != "c.txt" {
t.Errorf("baseName unix = %q", got)
}
if got := baseName(`C:\dir\f.txt`); got != "f.txt" {
t.Errorf("baseName win = %q", got)
}
if got := pathExt("a.png"); got != ".png" {
t.Errorf("pathExt = %q", got)
}
if got := pathExt("noext"); got != "" {
t.Errorf("pathExt noext = %q", got)
}
}
func TestBuildMIMEAlternative(t *testing.T) {
s := New(Config{})
msg := mailx.NewMessage().
To("t@e.com").
Subject("s").
Text("纯文本正文").
HTML("<p>html正文</p>").
Build()
data, err := s.buildMIME(msg, "f@e.com")
if err != nil {
t.Fatal(err)
}
out := string(data)
if !strings.Contains(out, "multipart/alternative") {
t.Errorf("should use multipart/alternative, got:\n%s", out)
}
if !strings.Contains(out, "text/plain; charset=UTF-8") {
t.Errorf("missing text/plain part, got:\n%s", out)
}
if !strings.Contains(out, "text/html; charset=UTF-8") {
t.Errorf("missing text/html part, got:\n%s", out)
}
}