Files
mailgo/internal/imap_server/backend.go
T
kevin 353bfa88f2 feat: 新增 SMTP/IMAP/POP3 协议调用日志(含攻击分析筛选)
- 每个连接记录一条日志:协议、端口、来源 IP、用户名、成功/失败、
  失败原因(密码错误/IP封禁/中继被拒/发件人伪造/未认证发信等)、
  操作摘要、消息数与会话时长
- 管理后台新增「协议日志」页:按协议/状态/IP/用户名/时间筛选,
  今日与历史成功/失败统计卡片,分页查看,可手动清理
- 后台每 6 小时自动清理超出 protocol_log_keep_days(默认30天)
  的日志;新增 [web] protocol_log_keep_days 配置项
- 修复 POP3 认证既有 bug:handleUSER 丢弃邮箱域名导致 PASS 永远失败
- 新增 store 单测、SMTP/POP3 端到端测试与模板渲染测试
2026-08-19 18:49:29 +08:00

877 lines
22 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 imap_server
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"net/mail"
"strings"
"time"
"mail_go/config"
"mail_go/internal/db"
"mail_go/internal/mailutil"
"mail_go/internal/store"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/emersion/go-imap/backend/backendutil"
asgomail "github.com/emersion/go-message/mail"
"github.com/emersion/go-message/textproto"
)
// ---------- imapBackend ----------
// imapBackend implements backend.Backend.
type imapBackend struct {
stores *store.Stores
banCfg config.BanConfig
port int
}
// Login authenticates a user by email and password.
func (b *imapBackend) Login(connInfo *imap.ConnInfo, username, password string) (backend.User, error) {
clientIP := store.ClientIPFromAddr(connInfo.RemoteAddr)
now := time.Now()
// 已封禁 IP 一律拒绝认证(防协议层暴力破解)
if banned, _ := b.stores.Bans.IsBanned(clientIP); banned {
b.recordLogin(clientIP, username, false, "IP已被封禁", "认证被拒绝(IP 已封禁)", 0, now)
return nil, backend.ErrInvalidCredentials
}
user, err := b.stores.Users.Authenticate(username, password)
if err != nil {
// 认证失败计数,达到阈值封禁(与 Web 登录共用 ban_entries
b.stores.RecordAuthFailure(clientIP, b.banCfg.MaxFailAttempts, b.banCfg.BanDurationMin)
b.recordLogin(clientIP, username, false, "用户名或密码错误", "LOGIN 失败", 0, now)
return nil, fmt.Errorf("invalid credentials: %w", err)
}
email := user.Username + "@"
domain, err := b.stores.Domains.GetByID(user.DomainID)
if err == nil {
email = user.Username + "@" + domain.Name
}
logID := b.recordLogin(clientIP, username, true, "", "LOGIN 成功", 0, now)
return &imapUser{
stores: b.stores,
id: user.ID,
email: email,
logID: logID,
clientIP: clientIP,
startedAt: now,
}, nil
}
// recordLogin 写入一条 IMAP 登录日志,返回新记录的 ID(失败时为 0)。
func (b *imapBackend) recordLogin(ip, username string, success bool, failReason, detail string, durationMs int64, at time.Time) uint {
entry := &db.ProtocolLog{
Protocol: db.ProtocolIMAP,
Port: b.port,
ClientIP: ip,
Username: username,
Success: success,
FailReason: failReason,
Detail: detail,
DurationMs: durationMs,
CreatedAt: at,
}
if err := b.stores.ProtocolLogs.Create(entry); err != nil {
log.Printf("IMAP: 写入协议日志失败: %v", err)
return 0
}
return entry.ID
}
// ---------- imapUser ----------
// imapUser implements backend.User.
type imapUser struct {
stores *store.Stores
id uint
email string
logID uint
clientIP string
startedAt time.Time
}
// Username returns the user's email address.
func (u *imapUser) Username() string {
return u.email
}
// ListMailboxes returns the standard mailbox list.
func (u *imapUser) ListMailboxes(subscribed bool) ([]backend.Mailbox, error) {
folders := []struct {
name string
delimiter string
attributes []string
}{
{"INBOX", "/", nil},
{"Sent", "/", []string{"\\Sent"}},
{"Drafts", "/", []string{"\\Drafts"}},
{"Trash", "/", []string{"\\Trash"}},
}
mailboxes := make([]backend.Mailbox, 0, len(folders))
for _, f := range folders {
mailboxes = append(mailboxes, &imapMailbox{
stores: u.stores,
user: u,
name: f.name,
delimiter: f.delimiter,
attributes: f.attributes,
})
}
return mailboxes, nil
}
// GetMailbox returns a mailbox by name.
func (u *imapUser) GetMailbox(name string) (backend.Mailbox, error) {
normalized, ok := canonicalMailboxName(name)
if !ok {
return nil, backend.ErrNoSuchMailbox
}
return &imapMailbox{
stores: u.stores,
user: u,
name: normalized,
delimiter: "/",
}, nil
}
func canonicalMailboxName(name string) (string, bool) {
switch strings.ToUpper(strings.TrimSpace(name)) {
case "INBOX":
return "INBOX", true
case "SENT":
return "Sent", true
case "DRAFTS":
return "Drafts", true
case "TRASH":
return "Trash", true
default:
return "", false
}
}
// CreateMailbox creates a new mailbox (not supported in this version).
func (u *imapUser) CreateMailbox(name string) error {
return fmt.Errorf("mailbox creation not supported")
}
// DeleteMailbox deletes a mailbox (not supported in this version).
func (u *imapUser) DeleteMailbox(name string) error {
return fmt.Errorf("mailbox deletion not supported")
}
// RenameMailbox renames a mailbox (not supported in this version).
func (u *imapUser) RenameMailbox(existingName, newName string) error {
return fmt.Errorf("mailbox rename not supported")
}
// Logout is called when the user session ends.
func (u *imapUser) Logout() error {
// 回填会话时长,登录记录在 Login 时已写入
if u.logID == 0 {
return nil
}
durationMs := time.Since(u.startedAt).Milliseconds()
if err := u.stores.ProtocolLogs.UpdateDuration(u.logID, durationMs); err != nil {
log.Printf("IMAP: 更新协议日志失败: %v", err)
}
return nil
}
// ---------- imapMailbox ----------
// imapMailbox implements backend.Mailbox.
type imapMailbox struct {
stores *store.Stores
user *imapUser
name string
delimiter string
attributes []string
// deleted tracks messages marked as \Deleted in this session
deleted map[uint]bool
}
// Name returns the mailbox name.
func (m *imapMailbox) Name() string {
return m.name
}
// Info returns mailbox metadata.
func (m *imapMailbox) Info() (*imap.MailboxInfo, error) {
attrs := m.attributes
if attrs == nil {
attrs = []string{}
}
return &imap.MailboxInfo{
Name: m.name,
Delimiter: m.delimiter,
Attributes: attrs,
}, nil
}
// Status returns mailbox status information.
func (m *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
status := imap.NewMailboxStatus(m.name, items)
status.Flags = []string{"\\Answered", "\\Flagged", "\\Deleted", "\\Seen", "\\Draft"}
status.PermanentFlags = []string{"\\Answered", "\\Flagged", "\\Deleted", "\\Seen", "\\Draft", "\\*"}
messages, err := m.stores.Mails.ListAllByUserAndFolder(m.user.id, m.name)
if err != nil {
return nil, err
}
status.Messages = uint32(len(messages))
var unseenCount uint32
for _, msg := range messages {
if !msg.IsRead {
unseenCount++
}
}
status.Unseen = unseenCount
status.Recent = 0
maxID, err := m.stores.Mails.MaxIDByUserAndFolder(m.user.id, m.name)
if err != nil {
return nil, err
}
status.UidNext = uint32(maxID + 1)
status.UidValidity = 1
return status, nil
}
// SetSubscribed sets the subscribed status (no-op for now).
func (m *imapMailbox) SetSubscribed(subscribed bool) error {
return nil
}
// Check is a no-op checkpoint.
func (m *imapMailbox) Check() error {
return nil
}
// ListMessages returns messages matching the sequence set and fetch items.
func (m *imapMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
defer close(ch)
// Fetch all messages in this mailbox
dbMessages, err := m.stores.Mails.ListAllByUserAndFolder(m.user.id, m.name)
if err != nil {
return err
}
if len(dbMessages) == 0 {
return nil
}
// Build a mapping of sequence number (1-based) to db.Message
type seqEntry struct {
seqNum uint32
msg *db.Message
}
entries := make([]seqEntry, len(dbMessages))
for i := range dbMessages {
entries[i] = seqEntry{
seqNum: uint32(i + 1),
msg: &dbMessages[i],
}
}
for _, entry := range entries {
var match bool
if uid {
match = seqset.Contains(uint32(entry.msg.ID))
} else {
match = seqset.Contains(entry.seqNum)
}
if !match {
continue
}
imapMsg, err := m.buildIMAPMessage(entry.msg, entry.seqNum, items)
if err != nil {
log.Printf("IMAP: error building message %d: %v", entry.msg.ID, err)
continue
}
ch <- imapMsg
}
return nil
}
// buildIMAPMessage constructs an imap.Message from a db.Message with the requested items.
func (m *imapMailbox) buildIMAPMessage(dbMsg *db.Message, seqNum uint32, items []imap.FetchItem) (*imap.Message, error) {
imapMsg := imap.NewMessage(seqNum, items)
imapMsg.Uid = uint32(dbMsg.ID)
imapMsg.Flags = m.getMessageFlags(dbMsg)
imapMsg.InternalDate = dbMsg.Date
rawMsg := messageRawData(dbMsg)
imapMsg.Size = uint32(len(rawMsg))
for _, item := range items {
switch item {
case imap.FetchUid, imap.FetchFlags, imap.FetchInternalDate, imap.FetchRFC822Size:
continue
case imap.FetchEnvelope:
hdr, _, err := headerAndBody(rawMsg)
if err == nil {
imapMsg.Envelope, _ = backendutil.FetchEnvelope(hdr)
}
if imapMsg.Envelope == nil {
imapMsg.Envelope = m.buildEnvelope(dbMsg)
}
case imap.FetchBody, imap.FetchBodyStructure:
hdr, body, err := headerAndBody(rawMsg)
if err == nil {
imapMsg.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure)
}
default:
section, err := imap.ParseBodySectionName(item)
if err != nil {
continue
}
hdr, body, err := headerAndBody(rawMsg)
if err != nil {
return nil, err
}
literal, _ := backendutil.FetchBodySection(hdr, body, section)
imapMsg.Body[section] = literal
}
}
return imapMsg, nil
}
func messageRawData(msg *db.Message) []byte {
if msg.RawData != "" {
return []byte(msg.RawData)
}
return buildRawMessage(msg)
}
func headerAndBody(raw []byte) (textproto.Header, io.Reader, error) {
body := bufio.NewReader(bytes.NewReader(raw))
hdr, err := textproto.ReadHeader(body)
return hdr, body, err
}
// getMessageFlags returns IMAP flags for a database message.
func (m *imapMailbox) getMessageFlags(dbMsg *db.Message) []string {
flags := make([]string, 0)
if dbMsg.IsRead {
flags = append(flags, "\\Seen")
}
if dbMsg.IsFlagged {
flags = append(flags, "\\Flagged")
}
if m.deleted != nil && m.deleted[dbMsg.ID] {
flags = append(flags, "\\Deleted")
}
return flags
}
// buildEnvelope constructs an imap.Envelope from a db.Message.
func (m *imapMailbox) buildEnvelope(dbMsg *db.Message) *imap.Envelope {
env := &imap.Envelope{
Date: dbMsg.Date,
Subject: dbMsg.Subject,
From: parseAddressList(dbMsg.FromAddr),
Sender: parseAddressList(dbMsg.FromAddr),
ReplyTo: parseAddressList(dbMsg.FromAddr),
To: parseAddressList(dbMsg.ToAddr),
Cc: parseAddressList(dbMsg.CcAddr),
MessageId: dbMsg.MessageID,
}
return env
}
// SearchMessages returns sequence numbers or UIDs of messages matching the criteria.
func (m *imapMailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) {
dbMessages, err := m.stores.Mails.ListAllByUserAndFolder(m.user.id, m.name)
if err != nil {
return nil, err
}
var results []uint32
for i, dbMsg := range dbMessages {
if m.matchesCriteria(&dbMsg, criteria) {
if uid {
results = append(results, uint32(dbMsg.ID))
} else {
results = append(results, uint32(i+1))
}
}
}
return results, nil
}
// matchesCriteria checks if a message matches the given search criteria.
func (m *imapMailbox) matchesCriteria(msg *db.Message, criteria *imap.SearchCriteria) bool {
// Check WithFlags (messages must have all these flags)
for _, flag := range criteria.WithFlags {
switch flag {
case "\\Seen":
if !msg.IsRead {
return false
}
case "\\Flagged":
if !msg.IsFlagged {
return false
}
case "\\Deleted":
if m.deleted == nil || !m.deleted[msg.ID] {
return false
}
}
}
// Check WithoutFlags (messages must NOT have any of these flags)
for _, flag := range criteria.WithoutFlags {
switch flag {
case "\\Seen":
if msg.IsRead {
return false
}
case "\\Flagged":
if msg.IsFlagged {
return false
}
case "\\Deleted":
if m.deleted != nil && m.deleted[msg.ID] {
return false
}
}
}
// Check date range
if !criteria.Since.IsZero() && msg.Date.Before(criteria.Since) {
return false
}
if !criteria.Before.IsZero() && !msg.Date.Before(criteria.Before) {
return false
}
// Check header fields
if criteria.Header != nil {
if subject := criteria.Header.Get("Subject"); subject != "" {
if !strings.Contains(strings.ToLower(msg.Subject), strings.ToLower(subject)) {
return false
}
}
if from := criteria.Header.Get("From"); from != "" {
if !strings.Contains(strings.ToLower(msg.FromAddr), strings.ToLower(from)) {
return false
}
}
if to := criteria.Header.Get("To"); to != "" {
if !strings.Contains(strings.ToLower(msg.ToAddr), strings.ToLower(to)) {
return false
}
}
}
// Check body text
for _, text := range criteria.Body {
bodyText := strings.ToLower(msg.TextBody + " " + msg.HtmlBody)
if !strings.Contains(bodyText, strings.ToLower(text)) {
return false
}
}
// Check generic text (searches headers + body)
for _, text := range criteria.Text {
allText := strings.ToLower(msg.Subject + " " + msg.FromAddr + " " + msg.ToAddr + " " + msg.TextBody + " " + msg.HtmlBody)
if !strings.Contains(allText, strings.ToLower(text)) {
return false
}
}
// Check NOT criteria
for _, notCrit := range criteria.Not {
if m.matchesCriteria(msg, notCrit) {
return false
}
}
// Check OR criteria (at least one must match)
for _, orPair := range criteria.Or {
if !m.matchesCriteria(msg, orPair[0]) && !m.matchesCriteria(msg, orPair[1]) {
return false
}
}
return true
}
// CreateMessage appends a new message to the mailbox (IMAP APPEND command).
func (m *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
// Read the message literal
data, err := io.ReadAll(body)
if err != nil {
return fmt.Errorf("failed to read message body: %w", err)
}
// Parse as MIME message
mr, err := asgomail.CreateReader(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to parse MIME message: %w", err)
}
header := mr.Header
fromAddr := mailutil.FormatAddressList(&header, "From")
toAddr := mailutil.FormatAddressList(&header, "To")
ccAddr := mailutil.FormatAddressList(&header, "Cc")
subject, _ := header.Subject()
messageID, _ := header.MessageID()
msgDate, _ := header.Date()
if msgDate.IsZero() {
msgDate = date
}
if msgDate.IsZero() {
msgDate = time.Now()
}
var textBody, htmlBody string
for {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
break
}
switch h := p.Header.(type) {
case *asgomail.InlineHeader:
contentType, params, _ := h.ContentType()
buf, _ := io.ReadAll(p.Body)
// 检测并转换字符集
charset := ""
if cs, ok := params["charset"]; ok {
charset = cs
}
decoded := mailutil.DecodeCharset(buf, charset)
if strings.HasPrefix(contentType, "text/plain") {
textBody = decoded
} else if strings.HasPrefix(contentType, "text/html") {
htmlBody = decoded
}
case *asgomail.AttachmentHeader:
// Attachments from APPEND are not saved in this simple implementation
}
}
if textBody == "" && htmlBody == "" {
textBody = string(data)
}
// Determine initial flag state
isRead := false
isFlagged := false
for _, flag := range flags {
switch flag {
case "\\Seen":
isRead = true
case "\\Flagged":
isFlagged = true
}
}
msg := &db.Message{
UserID: m.user.id,
MessageID: messageID,
Folder: m.name,
FromAddr: fromAddr,
ToAddr: toAddr,
CcAddr: ccAddr,
Subject: subject,
TextBody: textBody,
HtmlBody: htmlBody,
RawData: string(data),
IsRead: isRead,
IsFlagged: isFlagged,
Date: msgDate,
}
if err := m.stores.Mails.Create(msg); err != nil {
return fmt.Errorf("failed to create message: %w", err)
}
return nil
}
// UpdateMessagesFlags modifies flags on messages.
func (m *imapMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error {
if m.deleted == nil {
m.deleted = make(map[uint]bool)
}
dbMessages, err := m.stores.Mails.ListAllByUserAndFolder(m.user.id, m.name)
if err != nil {
return err
}
for i, dbMsg := range dbMessages {
var match bool
if uid {
match = seqset.Contains(uint32(dbMsg.ID))
} else {
match = seqset.Contains(uint32(i + 1))
}
if !match {
continue
}
flagSet := make(map[string]bool, len(flags))
for _, flag := range flags {
flagSet[flag] = true
}
applyFlag := func(flag string, enabled bool) {
switch flag {
case "\\Seen":
_ = m.stores.Mails.MarkReadState(dbMsg.ID, enabled)
case "\\Flagged":
_ = m.stores.Mails.MarkFlagged(dbMsg.ID, enabled)
case "\\Deleted":
if enabled {
m.deleted[dbMsg.ID] = true
} else {
delete(m.deleted, dbMsg.ID)
}
}
}
switch op {
case imap.SetFlags:
applyFlag("\\Seen", flagSet["\\Seen"])
applyFlag("\\Flagged", flagSet["\\Flagged"])
applyFlag("\\Deleted", flagSet["\\Deleted"])
case imap.AddFlags:
for flag := range flagSet {
applyFlag(flag, true)
}
case imap.RemoveFlags:
for flag := range flagSet {
applyFlag(flag, false)
}
}
}
return nil
}
// CopyMessages copies messages to another mailbox.
func (m *imapMailbox) CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error {
dest, ok := canonicalMailboxName(dest)
if !ok {
return backend.ErrNoSuchMailbox
}
dbMessages, err := m.stores.Mails.ListAllByUserAndFolder(m.user.id, m.name)
if err != nil {
return err
}
for i, dbMsg := range dbMessages {
var match bool
if uid {
match = seqset.Contains(uint32(dbMsg.ID))
} else {
match = seqset.Contains(uint32(i + 1))
}
if !match {
continue
}
// Create a copy in the destination mailbox
copyMsg := &db.Message{
UserID: m.user.id,
MessageID: dbMsg.MessageID,
Folder: dest,
FromAddr: dbMsg.FromAddr,
ToAddr: dbMsg.ToAddr,
CcAddr: dbMsg.CcAddr,
Subject: dbMsg.Subject,
TextBody: dbMsg.TextBody,
HtmlBody: dbMsg.HtmlBody,
RawData: dbMsg.RawData,
IsRead: dbMsg.IsRead,
IsFlagged: dbMsg.IsFlagged,
Date: dbMsg.Date,
}
if err := m.stores.Mails.Create(copyMsg); err != nil {
log.Printf("IMAP: failed to copy message %d to %s: %v", dbMsg.ID, dest, err)
}
}
return nil
}
// MoveMessages moves messages to another mailbox.
func (m *imapMailbox) MoveMessages(uid bool, seqset *imap.SeqSet, dest string) error {
dest, ok := canonicalMailboxName(dest)
if !ok {
return backend.ErrNoSuchMailbox
}
dbMessages, err := m.stores.Mails.ListAllByUserAndFolder(m.user.id, m.name)
if err != nil {
return err
}
for i, dbMsg := range dbMessages {
var match bool
if uid {
match = seqset.Contains(uint32(dbMsg.ID))
} else {
match = seqset.Contains(uint32(i + 1))
}
if !match {
continue
}
if err := m.stores.Mails.MoveToFolder(dbMsg.ID, dest); err != nil {
log.Printf("IMAP: failed to move message %d to %s: %v", dbMsg.ID, dest, err)
}
}
return nil
}
// Expunge permanently removes messages marked as \Deleted.
func (m *imapMailbox) Expunge() error {
if m.deleted == nil {
return nil
}
for msgID := range m.deleted {
if err := m.stores.Mails.Delete(msgID); err != nil {
log.Printf("IMAP: failed to expunge message %d: %v", msgID, err)
}
}
m.deleted = make(map[uint]bool)
return nil
}
// ---------- Helper functions ----------
// parseAddressList parses a comma-separated address string into imap.Address slice.
func parseAddressList(addrStr string) []*imap.Address {
if addrStr == "" {
return nil
}
addresses, err := mail.ParseAddressList(addrStr)
if err != nil {
// Fallback: treat the whole string as a single address
return []*imap.Address{{
MailboxName: addrStr,
HostName: "",
}}
}
result := make([]*imap.Address, 0, len(addresses))
for _, addr := range addresses {
parts := strings.SplitN(addr.Address, "@", 2)
mailbox := parts[0]
host := ""
if len(parts) > 1 {
host = parts[1]
}
result = append(result, &imap.Address{
PersonalName: addr.Name,
MailboxName: mailbox,
HostName: host,
})
}
return result
}
// buildRawMessage reconstructs a raw RFC822 message from a db.Message.
// If RawData is available, it uses the original raw data directly.
func buildRawMessage(msg *db.Message) []byte {
// 优先使用原始邮件数据
if msg.RawData != "" {
return []byte(msg.RawData)
}
// 降级:从字段重建
var buf bytes.Buffer
// Write headers
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(time.RFC1123Z)))
if msg.MessageID != "" {
buf.WriteString(fmt.Sprintf("Message-ID: %s\r\n", msg.MessageID))
}
buf.WriteString("MIME-Version: 1.0\r\n")
// Write body
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 buf.Bytes()
}
// buildRawHeader reconstructs just the header portion of a raw RFC822 message.
func buildRawHeader(msg *db.Message) []byte {
var buf bytes.Buffer
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(time.RFC1123Z)))
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))
} else if msg.TextBody != "" {
buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
} else if msg.HtmlBody != "" {
buf.WriteString("Content-Type: text/html; charset=utf-8\r\n")
}
return buf.Bytes()
}