问题:ResetFail 只在 Web 登录调用,协议层失败计数只增不减。 合法用户的客户端(手机 APP 用裸用户名重试、配置向导探测、输错 密码等)失败次数持续累积,每达到阈值就触发一次封禁档位,从第 4 次触发起真实封禁 30 分钟+——用户被反复误封,手机端表现为一直 卡在"正在接收邮件"。 修复: - IMAP/SMTP/POP3 认证成功路径调用 Bans.ResetFail(与 Web 一致) - 新增 UserStore.AuthenticateLogin:支持裸用户名(如 "kevin"), 唯一归属时自动解析到其域名;跨域名同名歧义时要求完整邮箱 - 新增 TestAuthenticateLoginBareUsername 单元测试
660 lines
18 KiB
Go
660 lines
18 KiB
Go
package pop3_server
|
||
|
||
import (
|
||
"bufio"
|
||
"crypto/tls"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"mail_go/config"
|
||
"mail_go/internal/connhub"
|
||
"mail_go/internal/db"
|
||
"mail_go/internal/imap_server"
|
||
"mail_go/internal/store"
|
||
"mail_go/internal/tlsutil"
|
||
)
|
||
|
||
// POP3Server implements a simple POP3 mail server over TCP.
|
||
type POP3Server struct {
|
||
listener net.Listener
|
||
stores *store.Stores
|
||
cfg config.POP3Config
|
||
banCfg config.BanConfig
|
||
tlsLoader *tlsutil.Loader
|
||
hub *connhub.Hub
|
||
pusher imap_server.Pusher // 邮件删除推送(IMAP 客户端同步),可空
|
||
wg sync.WaitGroup
|
||
}
|
||
|
||
// NewPOP3Server creates a new POP3 server instance. tlsLoader may be nil
|
||
// when TLS is not configured.
|
||
func NewPOP3Server(cfg config.POP3Config, stores *store.Stores, tlsLoader *tlsutil.Loader, banCfg config.BanConfig, hub *connhub.Hub, pusher imap_server.Pusher) *POP3Server {
|
||
return &POP3Server{stores: stores, cfg: cfg, banCfg: banCfg, tlsLoader: tlsLoader, hub: hub, pusher: pusher}
|
||
}
|
||
|
||
func (s *POP3Server) tlsConfig() (*tls.Config, error) {
|
||
if s.tlsLoader == nil {
|
||
return nil, fmt.Errorf("POP3 TLS certificate or key not configured")
|
||
}
|
||
// GetCertificate 每次握手按需重载证书,证书更新后无需重启服务
|
||
return &tls.Config{GetCertificate: s.tlsLoader.GetCertificate}, nil
|
||
}
|
||
|
||
// Start starts the POP3 server on the configured plain-text port.
|
||
func (s *POP3Server) Start() error {
|
||
var err error
|
||
s.listener, err = net.Listen("tcp", s.cfg.Addr)
|
||
if err != nil {
|
||
return fmt.Errorf("POP3 listen failed: %w", err)
|
||
}
|
||
port := parseAddrPort(s.cfg.Addr)
|
||
|
||
log.Printf("POP3 server listening on %s", s.cfg.Addr)
|
||
|
||
s.wg.Add(1)
|
||
go func() {
|
||
defer s.wg.Done()
|
||
for {
|
||
conn, err := s.listener.Accept()
|
||
if err != nil {
|
||
return
|
||
}
|
||
s.wg.Add(1)
|
||
go func() {
|
||
defer s.wg.Done()
|
||
s.handleConn(conn, port)
|
||
}()
|
||
}
|
||
}()
|
||
|
||
return nil
|
||
}
|
||
|
||
// StartTLS starts the POP3 server on the configured TLS port.
|
||
func (s *POP3Server) StartTLS() error {
|
||
tlsConfig, err := s.tlsConfig()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
listener, err := tls.Listen("tcp", s.cfg.TLSAddr, tlsConfig)
|
||
if err != nil {
|
||
return fmt.Errorf("POP3 TLS listen failed: %w", err)
|
||
}
|
||
port := parseAddrPort(s.cfg.TLSAddr)
|
||
|
||
log.Printf("POP3 TLS server listening on %s", s.cfg.TLSAddr)
|
||
|
||
s.wg.Add(1)
|
||
go func() {
|
||
defer s.wg.Done()
|
||
for {
|
||
conn, err := listener.Accept()
|
||
if err != nil {
|
||
return
|
||
}
|
||
s.wg.Add(1)
|
||
go func() {
|
||
defer s.wg.Done()
|
||
s.handleConn(conn, port)
|
||
}()
|
||
}
|
||
}()
|
||
|
||
return nil
|
||
}
|
||
|
||
// parseAddrPort 从监听地址解析端口号,失败返回 0。
|
||
func parseAddrPort(addr string) int {
|
||
_, portStr, err := net.SplitHostPort(addr)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
port, err := strconv.Atoi(portStr)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return port
|
||
}
|
||
|
||
// handleConn handles a single POP3 client connection.
|
||
func (s *POP3Server) handleConn(conn net.Conn, port int) {
|
||
defer conn.Close()
|
||
conn.SetDeadline(time.Now().Add(10 * time.Minute))
|
||
|
||
clientIP := store.ClientIPFromAddr(conn.RemoteAddr())
|
||
startedAt := time.Now()
|
||
|
||
// 已封禁 IP 直接拒绝(防协议层暴力破解)
|
||
if banned, _ := s.stores.Bans.IsBanned(clientIP); banned {
|
||
sendResponse(conn, "-ERR access denied")
|
||
s.writeProtocolLog(port, clientIP, "", false, "IP已被封禁", "连接被拒绝", 0, 0, startedAt)
|
||
return
|
||
}
|
||
|
||
// 连接追踪:注册到当前连接中心,连接结束时注销;
|
||
// 强制断开:关闭底层连接(STLS 后 conn 变量已指向 tlsConn,同样生效)。
|
||
activeConn := s.hub.Register("pop3", clientIP, port, false)
|
||
if activeConn != nil {
|
||
activeConn.SetDisconnect(func() { _ = conn.Close() })
|
||
}
|
||
|
||
// 会话状态(供协议日志汇总)
|
||
var (
|
||
authUser *db.User
|
||
authUsername string
|
||
authFailReason string
|
||
commandCount = make(map[string]int)
|
||
deletedCount int
|
||
)
|
||
|
||
reader := bufio.NewReader(conn)
|
||
var messages []pop3Message
|
||
var deleted map[int]bool
|
||
tlsActive := false
|
||
|
||
defer func() {
|
||
if activeConn != nil {
|
||
activeConn.Close()
|
||
}
|
||
}()
|
||
|
||
sendResponse(conn, "+OK MailGo POP3 server ready")
|
||
|
||
for {
|
||
line, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
break
|
||
}
|
||
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
parts := strings.SplitN(line, " ", 2)
|
||
cmd := strings.ToUpper(parts[0])
|
||
commandCount[cmd]++
|
||
activeConn.Touch()
|
||
arg := ""
|
||
if len(parts) > 1 {
|
||
arg = strings.TrimSpace(parts[1])
|
||
}
|
||
|
||
authenticated := authUser != nil && authUser.ID != 0
|
||
if !authenticated && requiresAuth(cmd) {
|
||
sendResponse(conn, "-ERR authentication required")
|
||
continue
|
||
}
|
||
|
||
switch cmd {
|
||
case "USER":
|
||
authUsername = arg
|
||
authUser, messages, deleted = s.handleUSER(conn, arg, authUser)
|
||
case "PASS":
|
||
authUser, messages, deleted = s.handlePASS(conn, arg, authUser)
|
||
if authUser == nil || authUser.ID == 0 {
|
||
if authFailReason == "" {
|
||
authFailReason = "用户名或密码错误"
|
||
}
|
||
} else {
|
||
authFailReason = ""
|
||
activeConn.SetUser(authUsername)
|
||
}
|
||
case "STAT":
|
||
s.handleSTAT(conn, messages, deleted)
|
||
case "LIST":
|
||
s.handleLIST(conn, arg, messages, deleted)
|
||
case "RETR":
|
||
s.handleRETR(conn, arg, messages, deleted)
|
||
case "DELE":
|
||
s.handleDELE(conn, arg, messages, deleted)
|
||
case "NOOP":
|
||
sendResponse(conn, "+OK")
|
||
case "RSET":
|
||
deleted = make(map[int]bool)
|
||
sendResponse(conn, "+OK")
|
||
case "QUIT":
|
||
deletedCount = s.expungeDeleted(messages, deleted, authUser)
|
||
sendResponse(conn, "+OK MailGo POP3 server signing off")
|
||
s.writeProtocolLog(port, clientIP, authUsername, authUser != nil && authUser.ID != 0, authFailReason,
|
||
pop3CommandDetail(commandCount, deletedCount), deletedCount,
|
||
time.Since(startedAt).Milliseconds(), time.Now())
|
||
return
|
||
case "CAPA":
|
||
s.handleCAPA(conn, tlsActive)
|
||
case "STLS":
|
||
if authenticated {
|
||
sendResponse(conn, "-ERR STLS not allowed after authentication")
|
||
continue
|
||
}
|
||
if tlsActive {
|
||
sendResponse(conn, "-ERR TLS already active")
|
||
continue
|
||
}
|
||
tlsConfig, err := s.tlsConfig()
|
||
if err != nil {
|
||
sendResponse(conn, "-ERR TLS not available")
|
||
continue
|
||
}
|
||
sendResponse(conn, "+OK Begin TLS negotiation")
|
||
tlsConn := tls.Server(conn, tlsConfig)
|
||
if err := tlsConn.Handshake(); err != nil {
|
||
s.writeProtocolLog(port, clientIP, authUsername, false, "TLS 握手失败",
|
||
pop3CommandDetail(commandCount, 0), 0, time.Since(startedAt).Milliseconds(), time.Now())
|
||
return
|
||
}
|
||
conn = tlsConn
|
||
reader = bufio.NewReader(conn)
|
||
tlsActive = true
|
||
activeConn.SetTLS(true)
|
||
case "TOP":
|
||
s.handleTOP(conn, arg, messages, deleted)
|
||
case "UIDL":
|
||
s.handleUIDL(conn, arg, messages, deleted)
|
||
default:
|
||
sendResponse(conn, "-ERR unknown command")
|
||
}
|
||
}
|
||
|
||
// 连接异常结束(未 QUIT)
|
||
success := authUser != nil && authUser.ID != 0 && authFailReason == ""
|
||
if success && authFailReason == "" && authUsername == "" && len(commandCount) == 0 {
|
||
success = true
|
||
}
|
||
s.writeProtocolLog(port, clientIP, authUsername, success, authFailReason,
|
||
pop3CommandDetail(commandCount, 0), 0, time.Since(startedAt).Milliseconds(), time.Now())
|
||
}
|
||
|
||
// pop3CommandDetail 汇总会话中执行的命令为可读摘要(计数,忽略 NOOP/CAPA)。
|
||
func pop3CommandDetail(counts map[string]int, deletedCount int) string {
|
||
var parts []string
|
||
for _, c := range []string{"USER", "PASS", "STAT", "LIST", "RETR", "TOP", "UIDL", "DELE", "RSET", "STLS", "QUIT"} {
|
||
n := counts[c]
|
||
if n == 0 {
|
||
continue
|
||
}
|
||
if n == 1 {
|
||
parts = append(parts, c)
|
||
} else {
|
||
parts = append(parts, fmt.Sprintf("%s×%d", c, n))
|
||
}
|
||
}
|
||
if deletedCount > 0 {
|
||
parts = append(parts, fmt.Sprintf("删除%d", deletedCount))
|
||
}
|
||
if len(parts) == 0 {
|
||
return "连接建立,无命令"
|
||
}
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
// writeProtocolLog 写入一条 POP3 协议调用日志。
|
||
func (s *POP3Server) writeProtocolLog(port int, ip, username string, success bool, failReason, detail string, msgCount int, durationMs int64, at time.Time) {
|
||
entry := &db.ProtocolLog{
|
||
Protocol: db.ProtocolPOP3,
|
||
Port: port,
|
||
ClientIP: ip,
|
||
Username: username,
|
||
Success: success,
|
||
FailReason: failReason,
|
||
Detail: detail,
|
||
MsgCount: msgCount,
|
||
DurationMs: durationMs,
|
||
CreatedAt: at,
|
||
}
|
||
if err := s.stores.ProtocolLogs.Create(entry); err != nil {
|
||
log.Printf("POP3: 写入协议日志失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func requiresAuth(cmd string) bool {
|
||
switch cmd {
|
||
case "STAT", "LIST", "RETR", "DELE", "RSET", "TOP", "UIDL":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (s *POP3Server) handleCAPA(conn net.Conn, tlsActive bool) {
|
||
sendResponse(conn, "+OK Capability list follows")
|
||
sendResponse(conn, "USER")
|
||
sendResponse(conn, "TOP")
|
||
sendResponse(conn, "UIDL")
|
||
sendResponse(conn, "RESP-CODES")
|
||
if !tlsActive && s.cfg.TLSCert != "" && s.cfg.TLSKey != "" {
|
||
sendResponse(conn, "STLS")
|
||
}
|
||
sendResponse(conn, ".")
|
||
}
|
||
|
||
// pop3Message holds a message and its computed size for POP3.
|
||
type pop3Message struct {
|
||
id uint
|
||
raw string
|
||
size int
|
||
message *db.Message
|
||
}
|
||
|
||
// loadMessages loads all INBOX messages for a user.
|
||
func (s *POP3Server) loadMessages(user *db.User) []pop3Message {
|
||
if user == nil {
|
||
return nil
|
||
}
|
||
|
||
dbMsgs, err := s.stores.Mails.ListAllByUserAndFolder(user.ID, "INBOX")
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
|
||
msgs := make([]pop3Message, 0, len(dbMsgs))
|
||
for i := range dbMsgs {
|
||
raw := string(buildRawMessage(&dbMsgs[i]))
|
||
msgs = append(msgs, pop3Message{
|
||
id: dbMsgs[i].ID,
|
||
raw: raw,
|
||
size: len(normalizePOP3Data(raw)),
|
||
message: &dbMsgs[i],
|
||
})
|
||
}
|
||
return msgs
|
||
}
|
||
|
||
// handleUSER processes the USER command.
|
||
func (s *POP3Server) handleUSER(conn net.Conn, username string, currentUser *db.User) (*db.User, []pop3Message, map[int]bool) {
|
||
if username == "" {
|
||
sendResponse(conn, "-ERR missing username")
|
||
return currentUser, nil, nil
|
||
}
|
||
|
||
user, err := s.stores.Users.GetByEmail(username)
|
||
if err != nil {
|
||
sendResponse(conn, "+OK")
|
||
return &db.User{Username: username}, nil, nil
|
||
}
|
||
|
||
// 保留完整的邮箱地址作为登录标识(PASS 阶段用 Authenticate 校验),
|
||
// user.ID 用于后续加载邮件。
|
||
user.Username = username
|
||
sendResponse(conn, "+OK")
|
||
return user, nil, nil
|
||
}
|
||
|
||
// handlePASS processes the PASS command.
|
||
func (s *POP3Server) handlePASS(conn net.Conn, password string, user *db.User) (*db.User, []pop3Message, map[int]bool) {
|
||
if user == nil {
|
||
sendResponse(conn, "-ERR no username given")
|
||
return nil, nil, nil
|
||
}
|
||
|
||
clientIP := store.ClientIPFromAddr(conn.RemoteAddr())
|
||
|
||
authUser, err := s.stores.Users.AuthenticateLogin(user.Username, password)
|
||
if err != nil {
|
||
// 认证失败计数,达到阈值按档位封禁(与 Web 登录共用 ban_entries)
|
||
s.stores.RecordAuthFailure(clientIP, s.banCfg.MaxFailAttempts, s.banCfg.BanDurationMin, "邮件协议认证失败次数过多")
|
||
sendResponse(conn, "-ERR authentication failed")
|
||
return nil, nil, nil
|
||
}
|
||
|
||
// 登录成功清零失败计数(与 Web 登录一致):防止合法用户 IP
|
||
// 因失败计数只增不减被反复误封。
|
||
s.stores.Bans.ResetFail(clientIP)
|
||
|
||
// 保留完整邮箱作为登录标识(与 handleUSER 一致),便于推送/日志使用
|
||
authUser.Username = user.Username
|
||
|
||
messages := s.loadMessages(authUser)
|
||
deleted := make(map[int]bool)
|
||
sendResponse(conn, fmt.Sprintf("+OK authenticated, %d messages", len(messages)))
|
||
return authUser, messages, deleted
|
||
}
|
||
|
||
// handleSTAT processes the STAT command.
|
||
func (s *POP3Server) handleSTAT(conn net.Conn, messages []pop3Message, deleted map[int]bool) {
|
||
count := 0
|
||
totalSize := 0
|
||
for i, msg := range messages {
|
||
if !deleted[i+1] {
|
||
count++
|
||
totalSize += msg.size
|
||
}
|
||
}
|
||
sendResponse(conn, fmt.Sprintf("+OK %d %d", count, totalSize))
|
||
}
|
||
|
||
// handleLIST processes the LIST command (with optional message number).
|
||
func (s *POP3Server) handleLIST(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||
if arg == "" {
|
||
sendResponse(conn, "+OK message list follows")
|
||
for i, msg := range messages {
|
||
if !deleted[i+1] {
|
||
sendResponse(conn, fmt.Sprintf("%d %d", i+1, msg.size))
|
||
}
|
||
}
|
||
sendResponse(conn, ".")
|
||
return
|
||
}
|
||
|
||
num, err := strconv.Atoi(arg)
|
||
if err != nil || num < 1 || num > len(messages) {
|
||
sendResponse(conn, "-ERR no such message")
|
||
return
|
||
}
|
||
if deleted[num] {
|
||
sendResponse(conn, "-ERR message deleted")
|
||
return
|
||
}
|
||
sendResponse(conn, fmt.Sprintf("+OK %d %d", num, messages[num-1].size))
|
||
}
|
||
|
||
// handleRETR processes the RETR command.
|
||
func (s *POP3Server) handleRETR(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||
num, err := strconv.Atoi(arg)
|
||
if err != nil || num < 1 || num > len(messages) {
|
||
sendResponse(conn, "-ERR no such message")
|
||
return
|
||
}
|
||
if deleted[num] {
|
||
sendResponse(conn, "-ERR message deleted")
|
||
return
|
||
}
|
||
|
||
msg := messages[num-1]
|
||
sendResponse(conn, fmt.Sprintf("+OK %d octets", msg.size))
|
||
writeDotStuffed(conn, msg.raw, nil)
|
||
conn.Write([]byte(".\r\n"))
|
||
}
|
||
|
||
// handleDELE processes the DELE command.
|
||
func (s *POP3Server) handleDELE(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||
num, err := strconv.Atoi(arg)
|
||
if err != nil || num < 1 || num > len(messages) {
|
||
sendResponse(conn, "-ERR no such message")
|
||
return
|
||
}
|
||
if deleted[num] {
|
||
sendResponse(conn, "-ERR message already deleted")
|
||
return
|
||
}
|
||
deleted[num] = true
|
||
sendResponse(conn, "+OK message marked for deletion")
|
||
}
|
||
|
||
// handleTOP processes the TOP command (headers + first N lines of body).
|
||
func (s *POP3Server) handleTOP(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||
parts := strings.Fields(arg)
|
||
if len(parts) < 1 {
|
||
sendResponse(conn, "-ERR syntax error")
|
||
return
|
||
}
|
||
|
||
num, err := strconv.Atoi(parts[0])
|
||
if err != nil || num < 1 || num > len(messages) {
|
||
sendResponse(conn, "-ERR no such message")
|
||
return
|
||
}
|
||
if deleted[num] {
|
||
sendResponse(conn, "-ERR message deleted")
|
||
return
|
||
}
|
||
|
||
nLines := 0
|
||
if len(parts) > 1 {
|
||
nLines, _ = strconv.Atoi(parts[1])
|
||
}
|
||
|
||
msg := messages[num-1]
|
||
header, body := splitHeaderBody(msg.raw)
|
||
sendResponse(conn, "+OK top of message follows")
|
||
writeDotStuffed(conn, header, nil)
|
||
conn.Write([]byte("\r\n"))
|
||
writeDotStuffed(conn, body, &nLines)
|
||
conn.Write([]byte(".\r\n"))
|
||
}
|
||
|
||
// handleUIDL processes the UIDL command.
|
||
func (s *POP3Server) handleUIDL(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||
if arg == "" {
|
||
sendResponse(conn, "+OK UIDL list follows")
|
||
for i, msg := range messages {
|
||
if !deleted[i+1] {
|
||
sendResponse(conn, fmt.Sprintf("%d %d", i+1, msg.id))
|
||
}
|
||
}
|
||
sendResponse(conn, ".")
|
||
return
|
||
}
|
||
|
||
num, err := strconv.Atoi(arg)
|
||
if err != nil || num < 1 || num > len(messages) {
|
||
sendResponse(conn, "-ERR no such message")
|
||
return
|
||
}
|
||
if deleted[num] {
|
||
sendResponse(conn, "-ERR message deleted")
|
||
return
|
||
}
|
||
sendResponse(conn, fmt.Sprintf("+OK %d %d", num, messages[num-1].id))
|
||
}
|
||
|
||
// expungeDeleted actually deletes messages that were marked for deletion,
|
||
// returning the number of messages deleted. 删除成功后向 IMAP 客户端推送
|
||
// Expunge 通知(序号为删除前在 INBOX 中的位置)。
|
||
func (s *POP3Server) expungeDeleted(messages []pop3Message, deleted map[int]bool, user *db.User) int {
|
||
if deleted == nil || user == nil || user.ID == 0 {
|
||
return 0
|
||
}
|
||
count := 0
|
||
var seqs []uint32
|
||
for seqNum, msgDeleted := range deleted {
|
||
if msgDeleted && seqNum >= 1 && seqNum <= len(messages) {
|
||
if err := s.stores.Mails.Delete(messages[seqNum-1].id); err != nil {
|
||
log.Printf("POP3: failed to delete message %d: %v", messages[seqNum-1].id, err)
|
||
continue
|
||
}
|
||
count++
|
||
seqs = append(seqs, uint32(seqNum))
|
||
}
|
||
}
|
||
if s.pusher != nil && len(seqs) > 0 && user.Username != "" {
|
||
s.pusher.PushExpunged(user.Username, "INBOX", seqs)
|
||
}
|
||
return count
|
||
}
|
||
|
||
// sendResponse writes a POP3 response line to the connection.
|
||
func sendResponse(conn net.Conn, line string) {
|
||
conn.Write([]byte(line + "\r\n"))
|
||
}
|
||
|
||
func normalizePOP3Data(raw string) string {
|
||
var b strings.Builder
|
||
writeDotStuffed(&b, raw, nil)
|
||
return b.String()
|
||
}
|
||
|
||
type stringWriter interface {
|
||
Write([]byte) (int, error)
|
||
}
|
||
|
||
func writeDotStuffed(w stringWriter, raw string, maxLines *int) {
|
||
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
||
raw = strings.ReplaceAll(raw, "\r", "\n")
|
||
lines := strings.Split(raw, "\n")
|
||
written := 0
|
||
for i, line := range lines {
|
||
if maxLines != nil && written >= *maxLines {
|
||
break
|
||
}
|
||
if i == len(lines)-1 && line == "" {
|
||
break
|
||
}
|
||
if strings.HasPrefix(line, ".") {
|
||
w.Write([]byte("."))
|
||
}
|
||
w.Write([]byte(line + "\r\n"))
|
||
written++
|
||
}
|
||
}
|
||
|
||
func splitHeaderBody(raw string) (string, string) {
|
||
if idx := strings.Index(raw, "\r\n\r\n"); idx >= 0 {
|
||
return raw[:idx], raw[idx+4:]
|
||
}
|
||
if idx := strings.Index(raw, "\n\n"); idx >= 0 {
|
||
return raw[:idx], raw[idx+2:]
|
||
}
|
||
return raw, ""
|
||
}
|
||
|
||
// buildRawMessage reconstructs a raw RFC822 message from a db.Message.
|
||
func buildRawMessage(msg *db.Message) []byte {
|
||
if msg.RawData != "" {
|
||
return []byte(msg.RawData)
|
||
}
|
||
|
||
var buf strings.Builder
|
||
buf.WriteString(fmt.Sprintf("From: %s\r\n", msg.FromAddr))
|
||
buf.WriteString(fmt.Sprintf("To: %s\r\n", msg.ToAddr))
|
||
if msg.CcAddr != "" {
|
||
buf.WriteString(fmt.Sprintf("Cc: %s\r\n", msg.CcAddr))
|
||
}
|
||
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", msg.Subject))
|
||
buf.WriteString(fmt.Sprintf("Date: %s\r\n", msg.Date.Format("Mon, 02 Jan 2006 15:04:05 -0700")))
|
||
if msg.MessageID != "" {
|
||
buf.WriteString(fmt.Sprintf("Message-ID: %s\r\n", msg.MessageID))
|
||
}
|
||
buf.WriteString("MIME-Version: 1.0\r\n")
|
||
|
||
if msg.HtmlBody != "" && msg.TextBody != "" {
|
||
boundary := fmt.Sprintf("mailgo_%d", msg.ID)
|
||
buf.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary))
|
||
buf.WriteString("\r\n")
|
||
buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||
buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")
|
||
buf.WriteString(msg.TextBody)
|
||
buf.WriteString("\r\n")
|
||
buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
|
||
buf.WriteString("Content-Type: text/html; charset=utf-8\r\n\r\n")
|
||
buf.WriteString(msg.HtmlBody)
|
||
buf.WriteString("\r\n")
|
||
buf.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
|
||
} else if msg.TextBody != "" {
|
||
buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")
|
||
buf.WriteString(msg.TextBody)
|
||
} else if msg.HtmlBody != "" {
|
||
buf.WriteString("Content-Type: text/html; charset=utf-8\r\n\r\n")
|
||
buf.WriteString(msg.HtmlBody)
|
||
} else {
|
||
buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")
|
||
}
|
||
return []byte(buf.String())
|
||
}
|