129 lines
4.8 KiB
Go
129 lines
4.8 KiB
Go
package parsemail
|
||
|
||
import (
|
||
"bytes"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.pcux.cn/xuans/Smail/utils/context"
|
||
"github.com/emersion/go-message"
|
||
"github.com/emersion/go-msgauth/dkim"
|
||
)
|
||
|
||
func TestAuditStableIMAPBytesAndCcPrivacy(t *testing.T) {
|
||
raw := auditRawMessage("Cc: Copy <copy@example.com>\r\n", "recipient@example.com")
|
||
parsed, err := NewEmailFromReader([]string{"recipient@example.com", "COPY@example.com", "hidden@example.com"}, strings.NewReader(raw), len(raw))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(parsed.Bcc) != 1 || parsed.Bcc[0].EmailAddress != "hidden@example.com" {
|
||
t.Fatal("抄送地址被错误归类为密送")
|
||
}
|
||
first, err := parsed.BuildIMAPBytes(nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for index := 0; index < 8; index++ {
|
||
next, err := parsed.BuildIMAPBytes(nil)
|
||
if err != nil || !bytes.Equal(first, next) {
|
||
t.Fatalf("同一封邮件 MIME 不稳定: %v", err)
|
||
}
|
||
}
|
||
if _, err := message.Read(bytes.NewReader(first)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func auditRawMessage(extraHeaders, to string) string {
|
||
return "From: sender@example.com\r\n" +
|
||
"To: " + to + "\r\n" +
|
||
"Message-ID: <audit-message@example.com>\r\n" +
|
||
"Subject: audit\r\n" + extraHeaders +
|
||
"Content-Type: text/plain; charset=UTF-8\r\n\r\nbody"
|
||
}
|
||
|
||
func TestAuditInvalidDKIMMustNotPassForSpecialDomain(t *testing.T) {
|
||
for _, domain := range []string{"ordinary.example.test", "test.domain"} {
|
||
t.Run(domain, func(t *testing.T) {
|
||
// 缺少必要签名标签,验证器在查询 DNS 前就应拒绝。
|
||
raw := auditRawMessage("DKIM-Signature: v=1; d="+domain+"\r\n", "recipient@example.com")
|
||
verifications, err := dkim.Verify(strings.NewReader(raw))
|
||
if err != nil || len(verifications) != 1 || verifications[0].Err == nil {
|
||
t.Fatalf("无效签名前置条件不成立: verifications=%v err=%v", verifications, err)
|
||
}
|
||
accepted := Check(&context.Context{}, strings.NewReader(raw))
|
||
t.Logf("domain=%q verifier_error=%v application_passed=%t", domain, verifications[0].Err, accepted)
|
||
if accepted {
|
||
t.Fatal("不能仅因签名域名是特殊常量就把无效 DKIM 判为通过")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAuditReplyToMustSurviveParseAndBuild(t *testing.T) {
|
||
raw := auditRawMessage("Reply-To: replies@example.com\r\n", "recipient@example.com")
|
||
parsed, err := NewEmailFromReader([]string{"recipient@example.com"}, strings.NewReader(raw), len(raw))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Logf("parsed_reply_to_count=%d", len(parsed.ReplyTo))
|
||
if len(parsed.ReplyTo) != 1 || parsed.ReplyTo[0].EmailAddress != "replies@example.com" {
|
||
t.Error("标准 Reply-To 在解析时丢失")
|
||
}
|
||
// 独立补入合法回复地址,检查重建层,避免解析失败遮挡第二层问题。
|
||
parsed.ReplyTo = []*User{{EmailAddress: "replies@example.com"}}
|
||
built, err := parsed.BuildBytes(nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
message, err := message.Read(bytes.NewReader(built))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
replyTo := message.Header.Get("Reply-To")
|
||
t.Logf("rebuilt_reply_to=%q", replyTo)
|
||
if !strings.Contains(replyTo, "replies@example.com") {
|
||
t.Error("合法 ReplyTo 字段在 MIME 重建时丢失")
|
||
}
|
||
}
|
||
|
||
func TestAuditQuotedRecipientNameMustNotSplitAddress(t *testing.T) {
|
||
raw := auditRawMessage("", `"Doe, Jane" <jane@example.com>`)
|
||
parsed, err := NewEmailFromReader([]string{"jane@example.com"}, strings.NewReader(raw), len(raw))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
addresses := make([]string, 0, len(parsed.To))
|
||
for _, recipient := range parsed.To {
|
||
addresses = append(addresses, recipient.EmailAddress)
|
||
}
|
||
t.Logf("quoted_recipient_addresses=%q", addresses)
|
||
if len(parsed.To) != 1 || parsed.To[0].EmailAddress != "jane@example.com" || parsed.To[0].Name != "Doe, Jane" {
|
||
t.Fatal("带引号姓名中的合法逗号被当作地址列表分隔符")
|
||
}
|
||
}
|
||
|
||
func TestAuditInlineAttachmentMustPreserveContentID(t *testing.T) {
|
||
email := &Email{
|
||
From: &User{EmailAddress: "sender@example.com"},
|
||
To: []*User{{EmailAddress: "recipient@example.com"}},
|
||
MsgID: "audit-cid@example.com", HTML: []byte(`<img src="cid:audit-inline">`),
|
||
Attachments: []*Attachment{{Filename: "inline.png", ContentType: "image/png", ContentID: "audit-inline", Content: []byte{1, 2, 3}}},
|
||
}
|
||
built, err := email.BuildBytes(nil, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
parsed, err := NewEmailFromReader([]string{"recipient@example.com"}, bytes.NewReader(built), len(built))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(parsed.Attachments) != 1 {
|
||
t.Fatalf("附件重建前置条件失败: count=%d", len(parsed.Attachments))
|
||
}
|
||
t.Logf("original_content_id=%q rebuilt_content_id=%q html_references_cid=%v", email.Attachments[0].ContentID, parsed.Attachments[0].ContentID, bytes.Contains(parsed.HTML, []byte("cid:audit-inline")))
|
||
if parsed.Attachments[0].ContentID != email.Attachments[0].ContentID {
|
||
t.Fatal("附件 Content-ID 在重建时丢失,HTML 的 cid 引用无法匹配")
|
||
}
|
||
}
|