Files
NewSzxcn-Email/apps/api/internal/app/mime.go
T
LanQin 8a042ae3dc feat(admin): 扩展后台管理与登录安全能力。
- 新增用户、域名、邮箱、别名、邮件、系统设置与模板的管理接口和页面。
- 支持双因素认证、Turnstile、人机验证与管理员 SMTP 测试。
- 增加无人收件/未注册邮件归档、Maildir 同步和数据库迁移支持。
- 更新前端导航、个人中心 2FA 配置以及相关部署示例。
2026-06-15 00:38:51 +08:00

181 lines
4.6 KiB
Go

package app
import (
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"mime"
"mime/multipart"
"net"
"net/smtp"
"net/textproto"
"strings"
"time"
)
type MIMEMessage struct {
From string
To []string
CC []string
BCC []string
Subject string
Text string
HTML string
MessageID string
Date time.Time
Attachments []AttachmentInput
}
func BuildMIME(m MIMEMessage) ([]byte, error) {
var buf bytes.Buffer
writeHeader := func(k, v string) {
if strings.TrimSpace(v) != "" {
fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
}
}
writeHeader("From", m.From)
writeHeader("To", strings.Join(m.To, ", "))
writeHeader("Cc", strings.Join(m.CC, ", "))
writeHeader("Subject", mime.QEncoding.Encode("utf-8", m.Subject))
writeHeader("Message-ID", m.MessageID)
writeHeader("Date", m.Date.Format(time.RFC1123Z))
writeHeader("MIME-Version", "1.0")
mixed := multipart.NewWriter(&buf)
writeHeader("Content-Type", `multipart/mixed; boundary="`+mixed.Boundary()+`"`)
buf.WriteString("\r\n")
var altBuf bytes.Buffer
alt := multipart.NewWriter(&altBuf)
textHeader := textprotoMIMEHeader(map[string]string{"Content-Type": `text/plain; charset="utf-8"`, "Content-Transfer-Encoding": "base64"})
textPart, err := alt.CreatePart(textHeader)
if err != nil {
return nil, err
}
writeBase64(textPart, []byte(m.Text))
htmlHeader := textprotoMIMEHeader(map[string]string{"Content-Type": `text/html; charset="utf-8"`, "Content-Transfer-Encoding": "base64"})
htmlPart, err := alt.CreatePart(htmlHeader)
if err != nil {
return nil, err
}
writeBase64(htmlPart, []byte(m.HTML))
if err := alt.Close(); err != nil {
return nil, err
}
altMixedHeader := textprotoMIMEHeader(map[string]string{"Content-Type": `multipart/alternative; boundary="` + alt.Boundary() + `"`})
altMixedPart, err := mixed.CreatePart(altMixedHeader)
if err != nil {
return nil, err
}
if _, err := altMixedPart.Write(altBuf.Bytes()); err != nil {
return nil, err
}
for _, att := range m.Attachments {
data, err := base64.StdEncoding.DecodeString(att.ContentBase64)
if err != nil {
return nil, err
}
contentType := att.ContentType
if contentType == "" {
contentType = "application/octet-stream"
}
filename := mime.QEncoding.Encode("utf-8", att.Filename)
h := textprotoMIMEHeader(map[string]string{
"Content-Type": contentType + `; name="` + filename + `"`,
"Content-Disposition": `attachment; filename="` + filename + `"`,
"Content-Transfer-Encoding": "base64",
})
part, err := mixed.CreatePart(h)
if err != nil {
return nil, err
}
writeBase64(part, data)
}
if err := mixed.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func textprotoMIMEHeader(values map[string]string) textproto.MIMEHeader {
h := textproto.MIMEHeader{}
for k, v := range values {
h.Set(k, v)
}
return h
}
func writeBase64(w io.Writer, data []byte) {
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(encoded, data)
for len(encoded) > 76 {
_, _ = w.Write(encoded[:76])
_, _ = w.Write([]byte("\r\n"))
encoded = encoded[76:]
}
_, _ = w.Write(encoded)
_, _ = w.Write([]byte("\r\n"))
}
func (a *App) sendSMTP(from string, recipients []string, mimeBytes []byte) error {
return sendSMTPWithConfig(a.cfg, from, recipients, mimeBytes)
}
func sendSMTPWithConfig(cfg Config, from string, recipients []string, mimeBytes []byte) error {
addr := net.JoinHostPort(cfg.SMTPHost, cfg.SMTPPort)
var auth smtp.Auth
if cfg.SMTPUsername != "" {
auth = smtp.PlainAuth("", cfg.SMTPUsername, cfg.SMTPPassword, cfg.SMTPHost)
}
if !cfg.SMTPRequireTLS {
return smtp.SendMail(addr, auth, from, recipients, mimeBytes)
}
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: cfg.SMTPHost, MinVersion: tls.VersionTLS12})
if err != nil {
return err
}
defer conn.Close()
client, err := smtp.NewClient(conn, cfg.SMTPHost)
if err != nil {
return err
}
defer client.Close()
if auth != nil {
if err := client.Auth(auth); err != nil {
return err
}
}
if err := client.Mail(from); err != nil {
return err
}
for _, rcpt := range recipients {
if err := client.Rcpt(rcpt); err != nil {
return err
}
}
wc, err := client.Data()
if err != nil {
return err
}
if _, err := wc.Write(mimeBytes); err != nil {
_ = wc.Close()
return err
}
if err := wc.Close(); err != nil {
return err
}
return client.Quit()
}
func htmlEscape(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\n", "<br>")
return s
}