package mailx import "fmt" // Message 邮件消息 type Message struct { From string // 发件人(可含显示名,如 "张三" ) 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 中用 引用) } // 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:" 引用 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 设置发件人(可含显示名,如 "张三" ) 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 中用 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 中用 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 }