Files
mailx/message.go
T
2026-08-15 01:38:05 +08:00

219 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}