This commit is contained in:
yun
2026-08-15 01:38:05 +08:00
parent 139330aee2
commit eb8f660ab5
57 changed files with 5401 additions and 1438 deletions
+218
View File
@@ -0,0 +1,218 @@
package mailx
import "fmt"
// Message 邮件消息
type Message struct {
From string // 发件人(可含显示名,如 "张三" <a@b.com>
To []string // 收件人(可含显示名)
Cc []string // 抄送(可含显示名)
Bcc []string // 密送(可含显示名)
Subject string // 主题
TextBody string // 纯文本正文(可选,推荐设置以便纯文本客户端阅读)
Body string // HTML 正文(可选,若同时设置 TextBody 则邮件含纯文本与 HTML 两个版本)
Headers map[string]string // 自定义邮件头(可选),如 {"List-Unsubscribe": "<...>"}
ReplyTo string // 回复地址
Attachments []Attachment // 附件
Inline []InlineImage // 内嵌图片(HTML 中用 <img src="cid:..."> 引用)
}
// Attachment 附件(普通附件,下载式)
type Attachment struct {
Name string // 附件名称(可选,默认取 Path 的文件名)
Path string // 附件文件路径(Path 与 Data 二选一)
Data []byte // 附件内容(Path 与 Data 二选一)
}
// InlineImage 内嵌图片(HTML 邮件正文中引用的图片,显示在正文内)
type InlineImage struct {
CID string // Content-IDHTML 中用 src="cid:<CID>" 引用
Name string // 图片名称(可选,默认取 Path 的文件名或 "inline"
Path string // 图片文件路径(Path 与 Data 二选一)
Data []byte // 图片内容(Path 与 Data 二选一)
MIMEType string // MIME 类型(可选,如 image/png;留空则由扩展名推断或默认 image/octet-stream
}
// 单封邮件的生产建议上限
const (
MaxRecipients = 50 // 收件人 + 抄送 + 密送的总上限,防止滥用
MaxAttachments = 20 // 附件数量上限
MaxMessageSize = 25 * 1024 * 1024 // 整封邮件(正文 + 附件)的最大字节数,默认 25MB
MaxHeaderCount = 20 // 自定义头数量上限
)
// Validate 校验消息必填项与基本约束
func (m *Message) Validate() error {
if len(m.To) == 0 {
return fmt.Errorf("%w: requires at least one recipient", ErrInvalidMessage)
}
if m.Subject == "" {
return fmt.Errorf("%w: requires a subject", ErrInvalidMessage)
}
if m.From != "" && !IsValidAddress(m.From) {
return fmt.Errorf("%w: invalid From address %q", ErrInvalidMessage, m.From)
}
for _, addr := range m.allRecipients() {
if !IsValidAddress(addr) {
return fmt.Errorf("%w: invalid recipient address %q", ErrInvalidMessage, addr)
}
}
total := len(m.To) + len(m.Cc) + len(m.Bcc)
if total > MaxRecipients {
return fmt.Errorf("%w: too many recipients (%d > %d)", ErrInvalidMessage, total, MaxRecipients)
}
if len(m.Attachments) > MaxAttachments {
return fmt.Errorf("%w: too many attachments (%d > %d)", ErrInvalidMessage, len(m.Attachments), MaxAttachments)
}
for _, inl := range m.Inline {
if inl.CID == "" {
return fmt.Errorf("%w: inline image requires a CID", ErrInvalidMessage)
}
}
if len(m.Headers) > MaxHeaderCount {
return fmt.Errorf("%w: too many custom headers (%d > %d)", ErrInvalidMessage, len(m.Headers), MaxHeaderCount)
}
if size := m.size(); size > MaxMessageSize {
return fmt.Errorf("%w: message too large (%d > %d bytes)", ErrInvalidMessage, size, MaxMessageSize)
}
return nil
}
// size 估算整封邮件的字节数(正文 + 附件 + 内嵌图片)
func (m *Message) size() int {
n := len(m.Body) + len(m.TextBody)
for _, att := range m.Attachments {
n += len(att.Data)
}
for _, inl := range m.Inline {
n += len(inl.Data)
}
return n
}
// allRecipients 汇总所有接收方地址
func (m *Message) allRecipients() []string {
out := make([]string, 0, len(m.To)+len(m.Cc)+len(m.Bcc))
out = append(out, m.To...)
out = append(out, m.Cc...)
out = append(out, m.Bcc...)
return out
}
// MessageBuilder 消息构建器,支持链式调用
type MessageBuilder struct {
msg *Message
}
// NewMessage 创建消息构建器
func NewMessage() *MessageBuilder {
return &MessageBuilder{msg: &Message{}}
}
// From 设置发件人(可含显示名,如 "张三" <a@b.com>
func (b *MessageBuilder) From(from string) *MessageBuilder {
b.msg.From = from
return b
}
// To 添加收件人
func (b *MessageBuilder) To(to ...string) *MessageBuilder {
b.msg.To = append(b.msg.To, to...)
return b
}
// Cc 添加抄送
func (b *MessageBuilder) Cc(cc ...string) *MessageBuilder {
b.msg.Cc = append(b.msg.Cc, cc...)
return b
}
// Bcc 添加密送
func (b *MessageBuilder) Bcc(bcc ...string) *MessageBuilder {
b.msg.Bcc = append(b.msg.Bcc, bcc...)
return b
}
// Subject 设置主题
func (b *MessageBuilder) Subject(subject string) *MessageBuilder {
b.msg.Subject = subject
return b
}
// Body 设置 HTML 正文(等价于 HTML)。
// 如需同时提供纯文本版本给不支持 HTML 的客户端,请再调用 Text。
func (b *MessageBuilder) Body(body string) *MessageBuilder {
b.msg.Body = body
return b
}
// Text 设置纯文本正文(可选,便于纯文本邮件客户端阅读)
func (b *MessageBuilder) Text(text string) *MessageBuilder {
b.msg.TextBody = text
return b
}
// HTML 设置正文(HTML
func (b *MessageBuilder) HTML(html string) *MessageBuilder {
b.msg.Body = html
return b
}
// ReplyTo 设置回复地址
func (b *MessageBuilder) ReplyTo(replyTo string) *MessageBuilder {
b.msg.ReplyTo = replyTo
return b
}
// Header 设置一个自定义邮件头(如 "List-Unsubscribe"、"X-Mailer")。
// 与标准头(From/To/Subject 等)重名时,自定义值不会覆盖标准头。
func (b *MessageBuilder) Header(key, value string) *MessageBuilder {
if b.msg.Headers == nil {
b.msg.Headers = make(map[string]string)
}
b.msg.Headers[key] = value
return b
}
// Attach 添加文件附件(按路径)
func (b *MessageBuilder) Attach(path string) *MessageBuilder {
b.msg.Attachments = append(b.msg.Attachments, Attachment{
Name: fileBaseName(path),
Path: path,
})
return b
}
// AttachBytes 添加内存附件(按字节内容)
func (b *MessageBuilder) AttachBytes(name string, data []byte) *MessageBuilder {
b.msg.Attachments = append(b.msg.Attachments, Attachment{
Name: name,
Data: data,
})
return b
}
// InlineImage 添加内嵌图片(按路径),HTML 中用 <img src="cid:<cid>">
func (b *MessageBuilder) InlineImage(cid, path string) *MessageBuilder {
b.msg.Inline = append(b.msg.Inline, InlineImage{
CID: cid,
Name: fileBaseName(path),
Path: path,
})
return b
}
// InlineImageBytes 添加内存内嵌图片,HTML 中用 <img src="cid:<cid>">
func (b *MessageBuilder) InlineImageBytes(cid, name string, data []byte) *MessageBuilder {
b.msg.Inline = append(b.msg.Inline, InlineImage{
CID: cid,
Name: name,
Data: data,
})
return b
}
// Build 生成消息
func (b *MessageBuilder) Build() *Message {
return b.msg
}