更新
This commit is contained in:
+218
@@ -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-ID,HTML 中用 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
|
||||
}
|
||||
Reference in New Issue
Block a user