深化smtp文件的支持
This commit is contained in:
@@ -15,7 +15,7 @@ func ExampleUsage() {
|
||||
smtpClient := NewSmtp()
|
||||
|
||||
// 配置SMTP设置
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.EmailOption) {
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.gmail.com",
|
||||
Port: "587", // STARTTLS
|
||||
@@ -37,7 +37,7 @@ func ExampleUsage() {
|
||||
Subject: "测试邮件 - Enhanced SMTP",
|
||||
Body: "<h1>这是一封测试邮件</h1><p>支持HTML格式和多种功能。</p>",
|
||||
ReplyTo: "noreply@example.com",
|
||||
Attachment: []interfaces.MessageAttachment{
|
||||
Attachment: []interfaces.Attachment{
|
||||
{Content: "path/to/attachment.pdf"},
|
||||
},
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func ExampleSSLUsage() {
|
||||
|
||||
smtpClient := NewSmtp()
|
||||
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.EmailOption) {
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.gmail.com",
|
||||
Port: "465", // SSL
|
||||
@@ -87,7 +87,7 @@ func ExampleEnterpriseEmail() {
|
||||
smtpClient := NewSmtp()
|
||||
|
||||
// 企业邮箱通常使用587端口和STARTTLS
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.EmailOption) {
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.exmail.qq.com", // 腾讯企业邮箱
|
||||
Port: "587",
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.yun.ink/pkg/mailx/interfaces"
|
||||
)
|
||||
|
||||
// 测试内容类型检测
|
||||
func TestContentTypeDetection(t *testing.T) {
|
||||
smtpClient := NewSmtp()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected interfaces.ContentType
|
||||
}{
|
||||
{
|
||||
name: "HTML with html tag",
|
||||
body: "<html><body>Test</body></html>",
|
||||
expected: interfaces.ContentTypeHTML,
|
||||
},
|
||||
{
|
||||
name: "HTML with div tag",
|
||||
body: "<div>Test content</div>",
|
||||
expected: interfaces.ContentTypeHTML,
|
||||
},
|
||||
{
|
||||
name: "HTML with img tag",
|
||||
body: "Check this image: <img src='test.jpg'>",
|
||||
expected: interfaces.ContentTypeHTML,
|
||||
},
|
||||
{
|
||||
name: "Plain text",
|
||||
body: "This is plain text without any HTML tags.",
|
||||
expected: interfaces.ContentTypeText,
|
||||
},
|
||||
{
|
||||
name: "Text with angle brackets",
|
||||
body: "Price: 100 < 200, Quality: A > B",
|
||||
expected: interfaces.ContentTypeText,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := smtpClient.detectContentType(tt.body)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %s, got %s for body: %s", tt.expected, result, tt.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 测试邮件头构建
|
||||
func TestBuildHeaders(t *testing.T) {
|
||||
smtpClient := NewSmtp()
|
||||
smtpClient.SetOption(context.Background(), interfaces.SetSmtp(&interfaces.EmailConfigDataSmtp{}))
|
||||
boundary := "test-boundary"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message interfaces.Message
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "HTML with inline images",
|
||||
message: interfaces.Message{
|
||||
Subject: "Test Subject",
|
||||
InlineImage: []interfaces.Attachment{{Name: "test.jpg"}},
|
||||
},
|
||||
expected: "multipart/related",
|
||||
},
|
||||
{
|
||||
name: "HTML with attachments",
|
||||
message: interfaces.Message{
|
||||
Subject: "Test Subject",
|
||||
Attachment: []interfaces.Attachment{{Name: "test.pdf"}},
|
||||
},
|
||||
expected: "multipart/mixed",
|
||||
},
|
||||
{
|
||||
name: "Simple message",
|
||||
message: interfaces.Message{
|
||||
Subject: "Test Subject",
|
||||
},
|
||||
expected: "multipart/alternative",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
headers := smtpClient.buildHeaders(tt.message, boundary)
|
||||
contentType := headers["Content-Type"]
|
||||
if !strings.Contains(contentType, tt.expected) {
|
||||
t.Errorf("Expected Content-Type to contain %s, got %s", tt.expected, contentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 测试附件类型处理
|
||||
func TestAttachmentTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
attachment interfaces.Attachment
|
||||
expectCID bool
|
||||
}{
|
||||
{
|
||||
name: "File attachment",
|
||||
attachment: interfaces.Attachment{
|
||||
Name: "document.pdf",
|
||||
Type: interfaces.AttachmentTypeFile,
|
||||
},
|
||||
expectCID: false,
|
||||
},
|
||||
{
|
||||
name: "Inline image",
|
||||
attachment: interfaces.Attachment{
|
||||
Name: "image.jpg",
|
||||
Type: interfaces.AttachmentTypeInline,
|
||||
CID: "test-image",
|
||||
},
|
||||
expectCID: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.expectCID && tt.attachment.CID == "" {
|
||||
t.Error("Expected CID for inline attachment")
|
||||
}
|
||||
if !tt.expectCID && tt.attachment.Type == interfaces.AttachmentTypeFile {
|
||||
// File attachments should not have CID
|
||||
if tt.attachment.CID != "" {
|
||||
t.Error("File attachment should not have CID")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 测试消息验证
|
||||
func TestMessageValidation2(t *testing.T) {
|
||||
smtpClient := NewSmtp()
|
||||
ctx := context.Background()
|
||||
|
||||
// 配置SMTP客户端
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.example.com",
|
||||
Port: "587",
|
||||
Username: "test@example.com",
|
||||
Password: "password",
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to configure SMTP: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message interfaces.Message
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid HTML message",
|
||||
message: interfaces.Message{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test",
|
||||
Body: "<h1>Test</h1>",
|
||||
BodyType: interfaces.ContentTypeHTML,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid text message",
|
||||
message: interfaces.Message{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test",
|
||||
Body: "Plain text",
|
||||
BodyType: interfaces.ContentTypeText,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Message with inline images",
|
||||
message: interfaces.Message{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test",
|
||||
Body: "<img src='cid:test'>",
|
||||
InlineImage: []interfaces.Attachment{
|
||||
{
|
||||
Name: "test.jpg",
|
||||
CID: "test",
|
||||
Type: interfaces.AttachmentTypeInline,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty recipients",
|
||||
message: interfaces.Message{
|
||||
Subject: "Test",
|
||||
Body: "Test body",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := smtpClient.validateMessage(tt.message)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateMessage() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 测试多部分邮件构建
|
||||
func TestMultipartEmailConstruction2(t *testing.T) {
|
||||
smtpClient := NewSmtp()
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := smtpClient.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.example.com",
|
||||
Port: "587",
|
||||
Username: "test@example.com",
|
||||
Password: "password",
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to configure SMTP: %v", err)
|
||||
}
|
||||
|
||||
message := interfaces.Message{
|
||||
To: []string{"recipient@example.com"},
|
||||
Subject: "Multipart Test",
|
||||
BodyType: interfaces.ContentTypeHTML,
|
||||
Body: "<h1>HTML Content</h1><img src='cid:test-img'>",
|
||||
TextBody: "Plain text version",
|
||||
InlineImage: []interfaces.Attachment{
|
||||
{
|
||||
Data: []byte("fake-image-data"),
|
||||
ContentType: "image/jpeg",
|
||||
Name: "test.jpg",
|
||||
CID: "test-img",
|
||||
Type: interfaces.AttachmentTypeInline,
|
||||
},
|
||||
},
|
||||
Attachment: []interfaces.Attachment{
|
||||
{
|
||||
Data: []byte("fake-pdf-data"),
|
||||
ContentType: "application/pdf",
|
||||
Name: "document.pdf",
|
||||
Type: interfaces.AttachmentTypeFile,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 测试邮件构建(不实际发送)
|
||||
boundary := "test-boundary"
|
||||
headers := smtpClient.buildHeaders(message, boundary)
|
||||
|
||||
// 验证Content-Type
|
||||
contentType := headers["Content-Type"]
|
||||
if !strings.Contains(contentType, "multipart/related") {
|
||||
t.Errorf("Expected multipart/related for message with inline images, got %s", contentType)
|
||||
}
|
||||
|
||||
// 验证必要的头部字段
|
||||
requiredHeaders := []string{"From", "To", "Subject", "Date", "MIME-Version", "Content-Type"}
|
||||
for _, header := range requiredHeaders {
|
||||
if _, exists := headers[header]; !exists {
|
||||
t.Errorf("Missing required header: %s", header)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试字节数据附件
|
||||
func TestByteDataAttachment(t *testing.T) {
|
||||
attachment := interfaces.Attachment{
|
||||
Data: []byte("test file content"),
|
||||
ContentType: "text/plain",
|
||||
Name: "test.txt",
|
||||
Type: interfaces.AttachmentTypeFile,
|
||||
}
|
||||
|
||||
if len(attachment.Data) == 0 {
|
||||
t.Error("Attachment data should not be empty")
|
||||
}
|
||||
|
||||
if attachment.ContentType != "text/plain" {
|
||||
t.Errorf("Expected content type text/plain, got %s", attachment.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
// 基准测试:HTML邮件构建性能
|
||||
func BenchmarkHTMLEmailConstruction(b *testing.B) {
|
||||
smtpClient := NewSmtp()
|
||||
ctx := context.Background()
|
||||
|
||||
smtpClient.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.example.com",
|
||||
Port: "587",
|
||||
Username: "test@example.com",
|
||||
Password: "password",
|
||||
}
|
||||
})
|
||||
|
||||
message := interfaces.Message{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Benchmark Test",
|
||||
BodyType: interfaces.ContentTypeHTML,
|
||||
Body: "<h1>Benchmark</h1><p>Performance test content</p>",
|
||||
TextBody: "Benchmark - Performance test content",
|
||||
InlineImage: []interfaces.Attachment{
|
||||
{
|
||||
Data: make([]byte, 1024), // 1KB fake image
|
||||
ContentType: "image/jpeg",
|
||||
Name: "test.jpg",
|
||||
CID: "test-img",
|
||||
Type: interfaces.AttachmentTypeInline,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
boundary := "benchmark-boundary"
|
||||
headers := smtpClient.buildHeaders(message, boundary)
|
||||
_ = headers
|
||||
}
|
||||
}
|
||||
|
||||
// 测试大附件处理
|
||||
func TestLargeAttachmentHandling(t *testing.T) {
|
||||
// 创建大附件数据(模拟)
|
||||
largeData := make([]byte, 1024*1024) // 1MB
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
attachment := interfaces.Attachment{
|
||||
Data: largeData,
|
||||
ContentType: "application/octet-stream",
|
||||
Name: "large_file.bin",
|
||||
Type: interfaces.AttachmentTypeFile,
|
||||
}
|
||||
|
||||
if len(attachment.Data) != 1024*1024 {
|
||||
t.Errorf("Expected 1MB data, got %d bytes", len(attachment.Data))
|
||||
}
|
||||
|
||||
// 测试是否超过限制(25MB)
|
||||
maxSize := 25 * 1024 * 1024
|
||||
if len(attachment.Data) > maxSize {
|
||||
t.Errorf("Attachment size %d exceeds limit %d", len(attachment.Data), maxSize)
|
||||
}
|
||||
}
|
||||
+192
-38
@@ -157,8 +157,8 @@ func (l *Smtp) sendPlain(ctx context.Context, message interfaces.Message) error
|
||||
buffer.WriteString(body)
|
||||
|
||||
for _, value := range message.Attachment {
|
||||
if err := l.writeAttachment(buffer, boundary, value.Content, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", value.Content, err)
|
||||
if err := l.writeAttachment(buffer, boundary, value, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", value.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -182,12 +182,20 @@ func (l *Smtp) sendWithTLS(ctx context.Context, message interfaces.Message) erro
|
||||
l.writeHeader(buffer, headers)
|
||||
|
||||
// 构建邮件体
|
||||
l.writeBody(buffer, boundary, message.Body)
|
||||
l.writeBody(buffer, boundary, message)
|
||||
|
||||
// 处理附件
|
||||
// 处理内嵌图片
|
||||
for _, inlineImg := range message.InlineImage {
|
||||
if err := l.writeAttachment(buffer, boundary, inlineImg, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process inline image %s: %v", inlineImg.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 处理普通附件
|
||||
for _, attachment := range message.Attachment {
|
||||
if err := l.writeAttachment(buffer, boundary, attachment.Content, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", attachment.Content, err)
|
||||
if err := l.writeAttachment(buffer, boundary, attachment, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", attachment.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -255,12 +263,20 @@ func (l *Smtp) sendWithSTARTTLS(ctx context.Context, message interfaces.Message)
|
||||
l.writeHeader(buffer, headers)
|
||||
|
||||
// 构建邮件体
|
||||
l.writeBody(buffer, boundary, message.Body)
|
||||
l.writeBody(buffer, boundary, message)
|
||||
|
||||
// 处理附件
|
||||
// 处理内嵌图片
|
||||
for _, inlineImg := range message.InlineImage {
|
||||
if err := l.writeAttachment(buffer, boundary, inlineImg, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process inline image %s: %v", inlineImg.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 处理普通附件
|
||||
for _, attachment := range message.Attachment {
|
||||
if err := l.writeAttachment(buffer, boundary, attachment.Content, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", attachment.Content, err)
|
||||
if err := l.writeAttachment(buffer, boundary, attachment, ctx); err != nil {
|
||||
l.Options.Logger.Errorf(ctx, "Failed to process attachment %s: %v", attachment.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -385,7 +401,15 @@ func (l *Smtp) buildHeaders(message interfaces.Message, boundary string) map[str
|
||||
headers["Subject"] = message.Subject
|
||||
headers["Date"] = time.Now().Format(time.RFC1123Z)
|
||||
headers["MIME-Version"] = "1.0"
|
||||
headers["Content-Type"] = "multipart/mixed; charset=UTF-8; boundary=" + boundary
|
||||
|
||||
// 根据内容类型和附件情况选择Content-Type
|
||||
if len(message.InlineImage) > 0 {
|
||||
headers["Content-Type"] = "multipart/related; charset=UTF-8; boundary=" + boundary
|
||||
} else if len(message.Attachment) > 0 {
|
||||
headers["Content-Type"] = "multipart/mixed; charset=UTF-8; boundary=" + boundary
|
||||
} else {
|
||||
headers["Content-Type"] = "multipart/alternative; charset=UTF-8; boundary=" + boundary
|
||||
}
|
||||
|
||||
if message.ReplyTo != "" {
|
||||
headers["Reply-To"] = message.ReplyTo
|
||||
@@ -405,52 +429,182 @@ func (l *Smtp) writeHeader(buffer *bytes.Buffer, headers map[string]string) {
|
||||
}
|
||||
|
||||
// 写入邮件体
|
||||
func (l *Smtp) writeBody(buffer *bytes.Buffer, boundary, body string) {
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||||
buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n")
|
||||
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
||||
buffer.WriteString(body + "\r\n")
|
||||
func (l *Smtp) writeBody(buffer *bytes.Buffer, boundary string, message interfaces.Message) {
|
||||
// 检测内容类型
|
||||
bodyType := message.BodyType
|
||||
if bodyType == interfaces.ContentTypeAuto {
|
||||
bodyType = l.detectContentType(message.Body)
|
||||
}
|
||||
|
||||
// 如果有纯文本版本,创建 multipart/alternative
|
||||
if message.TextBody != "" && bodyType == interfaces.ContentTypeHTML {
|
||||
altBoundary := "alt-" + boundary
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||||
buffer.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=%s\r\n\r\n", altBoundary))
|
||||
|
||||
// 纯文本版本
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", altBoundary))
|
||||
buffer.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
|
||||
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
||||
buffer.WriteString(message.TextBody + "\r\n")
|
||||
|
||||
// HTML版本
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", altBoundary))
|
||||
buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n")
|
||||
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
||||
buffer.WriteString(message.Body + "\r\n")
|
||||
|
||||
buffer.WriteString(fmt.Sprintf("--%s--\r\n", altBoundary))
|
||||
} else {
|
||||
// 单一内容类型
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||||
if bodyType == interfaces.ContentTypeHTML {
|
||||
buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n")
|
||||
} else {
|
||||
buffer.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
|
||||
}
|
||||
buffer.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
|
||||
buffer.WriteString(message.Body + "\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 检测内容类型
|
||||
func (l *Smtp) detectContentType(body string) interfaces.ContentType {
|
||||
if strings.Contains(body, "<html>") || strings.Contains(body, "<HTML>") ||
|
||||
strings.Contains(body, "<body>") || strings.Contains(body, "<BODY>") ||
|
||||
strings.Contains(body, "<div>") || strings.Contains(body, "<p>") ||
|
||||
strings.Contains(body, "<br>") || strings.Contains(body, "<img") {
|
||||
return interfaces.ContentTypeHTML
|
||||
}
|
||||
return interfaces.ContentTypeText
|
||||
}
|
||||
|
||||
// 写入附件
|
||||
func (l *Smtp) writeAttachment(buffer *bytes.Buffer, boundary, fileName string, ctx context.Context) error {
|
||||
// 检查文件大小
|
||||
stat, err := os.Stat(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file stat failed: %w", err)
|
||||
func (l *Smtp) writeAttachment(buffer *bytes.Buffer, boundary string, attachment interfaces.Attachment, ctx context.Context) error {
|
||||
if attachment.Type == interfaces.AttachmentTypeInline {
|
||||
return l.writeInlineImage(buffer, boundary, attachment, ctx)
|
||||
}
|
||||
if stat.Size() > MaxAttachmentSize {
|
||||
return fmt.Errorf("attachment too large: %d bytes (max: %d)", stat.Size(), MaxAttachmentSize)
|
||||
return l.writeFileAttachment(buffer, boundary, attachment, ctx)
|
||||
}
|
||||
|
||||
// 写入普通附件
|
||||
func (l *Smtp) writeFileAttachment(buffer *bytes.Buffer, boundary string, attachment interfaces.Attachment, ctx context.Context) error {
|
||||
var data []byte
|
||||
var err error
|
||||
var fileName string
|
||||
|
||||
// 获取数据
|
||||
if len(attachment.Data) > 0 {
|
||||
data = attachment.Data
|
||||
fileName = attachment.Name
|
||||
} else {
|
||||
data, err = l.readAttachmentFile(attachment.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileName = filepath.Base(attachment.Content)
|
||||
}
|
||||
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file failed: %w", err)
|
||||
if attachment.Name != "" {
|
||||
fileName = attachment.Name
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
baseName := filepath.Base(fileName)
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(fileName))
|
||||
// 检测 MIME 类型
|
||||
mimeType := attachment.ContentType
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
mimeType = mime.TypeByExtension(filepath.Ext(fileName))
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||||
buffer.WriteString("Content-Transfer-Encoding: base64\r\n")
|
||||
buffer.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\r\n", baseName))
|
||||
buffer.WriteString(fmt.Sprintf("Content-Type: %s; name=%s\r\n\r\n", mimeType, baseName))
|
||||
buffer.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\r\n", fileName))
|
||||
buffer.WriteString(fmt.Sprintf("Content-Type: %s; name=%s\r\n\r\n", mimeType, fileName))
|
||||
|
||||
// 流式编码以节省内存
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, &lineWrapper{buffer, 0})
|
||||
if _, err := io.Copy(encoder, file); err != nil {
|
||||
return fmt.Errorf("encode file failed: %w", err)
|
||||
// base64编码
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
for i := 0; i < len(encoded); i += 76 {
|
||||
end := i + 76
|
||||
if end > len(encoded) {
|
||||
end = len(encoded)
|
||||
}
|
||||
buffer.WriteString(encoded[i:end] + "\r\n")
|
||||
}
|
||||
encoder.Close()
|
||||
buffer.WriteString("\r\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入内嵌图片
|
||||
func (l *Smtp) writeInlineImage(buffer *bytes.Buffer, boundary string, attachment interfaces.Attachment, ctx context.Context) error {
|
||||
var data []byte
|
||||
var err error
|
||||
var fileName string
|
||||
|
||||
// 获取数据
|
||||
if len(attachment.Data) > 0 {
|
||||
data = attachment.Data
|
||||
fileName = attachment.Name
|
||||
} else {
|
||||
data, err = l.readAttachmentFile(attachment.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileName = filepath.Base(attachment.Content)
|
||||
}
|
||||
|
||||
if attachment.Name != "" {
|
||||
fileName = attachment.Name
|
||||
}
|
||||
|
||||
// 检测 MIME 类型
|
||||
mimeType := attachment.ContentType
|
||||
if mimeType == "" {
|
||||
mimeType = mime.TypeByExtension(filepath.Ext(fileName))
|
||||
if mimeType == "" {
|
||||
mimeType = "image/jpeg" // 默认图片类型
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 Content-ID
|
||||
cid := attachment.CID
|
||||
if cid == "" {
|
||||
cid = fmt.Sprintf("img_%d_%s", time.Now().UnixNano(), strings.ReplaceAll(fileName, ".", "_"))
|
||||
}
|
||||
|
||||
buffer.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||||
buffer.WriteString("Content-Transfer-Encoding: base64\r\n")
|
||||
buffer.WriteString(fmt.Sprintf("Content-Disposition: inline; filename=%s\r\n", fileName))
|
||||
buffer.WriteString(fmt.Sprintf("Content-Type: %s; name=%s\r\n", mimeType, fileName))
|
||||
buffer.WriteString(fmt.Sprintf("Content-ID: <%s>\r\n\r\n", cid))
|
||||
|
||||
// base64编码
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
for i := 0; i < len(encoded); i += 76 {
|
||||
end := i + 76
|
||||
if end > len(encoded) {
|
||||
end = len(encoded)
|
||||
}
|
||||
buffer.WriteString(encoded[i:end] + "\r\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 读取附件文件
|
||||
func (l *Smtp) readAttachmentFile(fileName string) ([]byte, error) {
|
||||
stat, err := os.Stat(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("file stat failed: %w", err)
|
||||
}
|
||||
if stat.Size() > MaxAttachmentSize {
|
||||
return nil, fmt.Errorf("attachment too large: %d bytes (max: %d)", stat.Size(), MaxAttachmentSize)
|
||||
}
|
||||
|
||||
return os.ReadFile(fileName)
|
||||
}
|
||||
|
||||
// 行包装器,用于base64编码时的换行
|
||||
type lineWrapper struct {
|
||||
w io.Writer
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestSmtpEnhanced(t *testing.T) {
|
||||
}
|
||||
|
||||
// 测试不完整配置
|
||||
_, err = smtp.SetOption(ctx, func(opt *interfaces.EmailOption) {
|
||||
_, err = smtp.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.example.com",
|
||||
// 缺少用户名和密码
|
||||
@@ -32,7 +32,7 @@ func TestSmtpEnhanced(t *testing.T) {
|
||||
}
|
||||
|
||||
// 测试完整配置
|
||||
_, err = smtp.SetOption(ctx, func(opt *interfaces.EmailOption) {
|
||||
_, err = smtp.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.example.com",
|
||||
Port: "587",
|
||||
@@ -51,7 +51,7 @@ func TestMessageValidation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 设置有效配置
|
||||
smtp.SetOption(ctx, func(opt *interfaces.EmailOption) {
|
||||
smtp.SetOption(ctx, func(opt *interfaces.Options) {
|
||||
opt.Smtp = &interfaces.EmailConfigDataSmtp{
|
||||
Host: "smtp.example.com",
|
||||
Port: "587",
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ func TestMail(t *testing.T) {
|
||||
ReplyTo: "huangxinyun@dreaminglife.cn",
|
||||
Subject: "测试邮件",
|
||||
Body: "这是测试邮件", //string(by),
|
||||
Attachment: []interfaces.MessageAttachment{
|
||||
Attachment: []interfaces.Attachment{
|
||||
// {
|
||||
// Name: "/code/statistic/out.xlsx",
|
||||
// ContentType: "",
|
||||
|
||||
Reference in New Issue
Block a user