Files

331 lines
9.2 KiB
Go
Raw Permalink Normal View History

2026-08-15 01:38:05 +08:00
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)
}
}