Files
mailgo/internal/imap_server/backend.go
T
kevin 3f28ec20f4 fix(security): 修复 P2 中危项(cookie/协议限速/路径遍历/默认口令/中继TLS/安全头/信息泄露)
- 会话 cookie 增加 Secure 标志;新增 [web].cookie_secure 配置
  (默认 true,仅本地 HTTP 调试关闭;缺失字段按安全默认处理)
- SMTP/IMAP/POP3 认证接入封禁体系(store.RecordAuthFailure 与 Web
  共用 ban_entries):失败计数达 ban.max_fail_attempts 即封禁 IP,
  已封禁 IP 拒绝认证,堵住协议层暴力破解
- 附件存储路径遍历防护重写:FullPath 白名单校验(UUID 文件名格式)
  + baseDir 前缀兜底,非法路径返回错误;Save 扩展名白名单化
- 初始管理员不再使用 admin/admin:密码取 MAILGO_ADMIN_PASSWORD 或
  随机生成并打印一次;新增 MustChangePassword 首登强制改密
  (管理员重置密码同样触发)
- 外发中继默认验证 TLS 证书(保护 AUTH 凭据,防 MITM),直投 MX
  保持机会式 TLS;新增 outbound.relay_tls_insecure 开关(默认 false)
- 新增安全响应头中间件:HSTS、X-Frame-Options DENY、nosniff、
  Referrer-Policy、基础 CSP(frame-ancestors 'none' 防点击劫持,
  connect-src/form-action 'self' 防数据外泄)
- LDAP/OAuth 登录错误统一为通用文案,原始错误只写日志,
  不再回显邮箱/内部细节(防用户枚举与信息泄露)
- 新增 25 个回归测试:cookie 标志、封禁阈值、路径遍历用例、
  中继 TLS 验证(自签证书 STARTTLS 集成)、安全头、OAuth 文案

部署注意:升级后所有会话失效需重新登录;若直接以 HTTP 提供
服务需显式配置 cookie_secure = false。
2026-08-19 16:45:21 +08:00

837 lines
21 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
}
// 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)
// 已封禁 IP 一律拒绝认证(防协议层暴力破解)
if banned, _ := b.stores.Bans.IsBanned(clientIP); banned {
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)
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
}
return &imapUser{
stores: b.stores,
id: user.ID,
email: email,
}, nil
}
// ---------- imapUser ----------
// imapUser implements backend.User.
type imapUser struct {
stores *store.Stores
id uint
email string
}
// 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 {
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()
}