更新
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
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 签名 base64(0x89 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user