This commit is contained in:
2026-06-01 20:43:01 +08:00
parent 0e9b62f5cf
commit 3dcc3f9a35
7 changed files with 137 additions and 15 deletions
+1
View File
@@ -59,6 +59,7 @@ type Message struct {
Subject string `gorm:"size:1024" json:"subject"`
TextBody string `gorm:"type:text" json:"text_body"`
HtmlBody string `gorm:"type:text" json:"html_body"`
RawData string `gorm:"type:mediumtext" json:"raw_data"`
IsRead bool `gorm:"default:false" json:"is_read"`
IsFlagged bool `gorm:"default:false" json:"is_flagged"`
Date time.Time `json:"date"`
+21 -6
View File
@@ -10,6 +10,7 @@ import (
"time"
"mail_go/internal/db"
"mail_go/internal/mailutil"
"mail_go/internal/store"
"github.com/emersion/go-imap"
@@ -502,9 +503,9 @@ func (m *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Li
}
header := mr.Header
fromAddr := header.Get("From")
toAddr := header.Get("To")
ccAddr := header.Get("Cc")
fromAddr := mailutil.FormatAddressList(&header, "From")
toAddr := mailutil.FormatAddressList(&header, "To")
ccAddr := mailutil.FormatAddressList(&header, "Cc")
subject, _ := header.Subject()
messageID, _ := header.MessageID()
msgDate, _ := header.Date()
@@ -527,12 +528,18 @@ func (m *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Li
switch h := p.Header.(type) {
case *asgomail.InlineHeader:
contentType, _, _ := h.ContentType()
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 = string(buf)
textBody = decoded
} else if strings.HasPrefix(contentType, "text/html") {
htmlBody = string(buf)
htmlBody = decoded
}
case *asgomail.AttachmentHeader:
// Attachments from APPEND are not saved in this simple implementation
@@ -565,6 +572,7 @@ func (m *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Li
Subject: subject,
TextBody: textBody,
HtmlBody: htmlBody,
RawData: string(data),
IsRead: isRead,
IsFlagged: isFlagged,
Date: msgDate,
@@ -717,7 +725,14 @@ func parseAddressList(addrStr string) []*imap.Address {
}
// 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
+74
View File
@@ -0,0 +1,74 @@
// Package mailutil provides utilities for email processing:
// charset conversion and RFC 2047 address formatting.
package mailutil
import (
"fmt"
"strings"
"unicode/utf8"
asgomail "github.com/emersion/go-message/mail"
"golang.org/x/text/encoding/htmlindex"
)
// DecodeCharset converts bytes from the given charset to a UTF-8 string.
// If charset is empty, "utf-8", or "us-ascii", it returns string(buf) directly.
// If the bytes are already valid UTF-8, no conversion is attempted.
func DecodeCharset(buf []byte, charset string) string {
cs := strings.ToLower(strings.Trim(charset, `"'`))
if cs == "" || cs == "utf-8" || cs == "us-ascii" {
return string(buf)
}
// 已是合法 UTF-8,无需转换
if utf8.Valid(buf) {
return string(buf)
}
enc, err := htmlindex.Get(cs)
if err != nil {
// 未知字符集,原样返回
return string(buf)
}
decoded, err := enc.NewDecoder().Bytes(buf)
if err != nil {
// 解码失败,原样返回
return string(buf)
}
return string(decoded)
}
// FormatAddressList 解析 RFC 2047 编码的地址列表头并格式化为可读字符串。
// 例如: "=?gb2312?B?zuIgzsS35Q==?= <a@b.com>" → "吴文锋 <a@b.com>"
func FormatAddressList(header *asgomail.Header, key string) string {
addrs, err := header.AddressList(key)
if err != nil || len(addrs) == 0 {
// 降级:返回原始值
return header.Get(key)
}
var parts []string
for _, addr := range addrs {
if addr.Name != "" {
parts = append(parts, fmt.Sprintf("%s <%s>", addr.Name, addr.Address))
} else {
parts = append(parts, addr.Address)
}
}
return strings.Join(parts, ", ")
}
// ExtractCharset 从 Content-Type 头值中提取 charset 参数。
// 例如: "text/plain; charset=gb2312" → "gb2312"
func ExtractCharset(contentType string) string {
parts := strings.Split(contentType, ";")
for _, part := range parts[1:] {
part = strings.TrimSpace(part)
if strings.HasPrefix(strings.ToLower(part), "charset=") {
return strings.Trim(strings.TrimPrefix(part, "charset="), `"'`)
}
}
return ""
}
+15 -7
View File
@@ -11,6 +11,7 @@ import (
"mail_go/config"
"mail_go/internal/db"
"mail_go/internal/mailutil"
"mail_go/internal/store"
"mail_go/internal/storage"
@@ -166,12 +167,12 @@ func (s *smtpSession) Data(r io.Reader) error {
return fmt.Errorf("failed to parse MIME message: %w", err)
}
// Extract headers from the top-level mail header
// Extract headers — 使用 AddressList 解码 RFC 2047 编码的地址
header := mr.Header
fromAddr := header.Get("From")
toAddr := header.Get("To")
ccAddr := header.Get("Cc")
fromAddr := mailutil.FormatAddressList(&header, "From")
toAddr := mailutil.FormatAddressList(&header, "To")
ccAddr := mailutil.FormatAddressList(&header, "Cc")
subject, _ := header.Subject()
messageID, _ := header.MessageID()
date, _ := header.Date()
@@ -195,16 +196,22 @@ func (s *smtpSession) Data(r io.Reader) error {
switch h := p.Header.(type) {
case *mail.InlineHeader:
contentType, _, _ := h.ContentType()
contentType, params, _ := h.ContentType()
buf, readErr := io.ReadAll(p.Body)
if readErr != nil {
log.Printf("SMTP: error reading inline part: %v", readErr)
continue
}
// 检测并转换字符集
charset := ""
if cs, ok := params["charset"]; ok {
charset = cs
}
decoded := mailutil.DecodeCharset(buf, charset)
if strings.HasPrefix(contentType, "text/plain") {
textBody = string(buf)
textBody = decoded
} else if strings.HasPrefix(contentType, "text/html") {
htmlBody = string(buf)
htmlBody = decoded
}
case *mail.AttachmentHeader:
@@ -257,6 +264,7 @@ func (s *smtpSession) Data(r io.Reader) error {
Subject: subject,
TextBody: textBody,
HtmlBody: htmlBody,
RawData: string(data),
IsRead: false,
IsFlagged: false,
Date: date,
+10 -1
View File
@@ -6,6 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看邮件 - MailGo 管理后台</title>
{{template "styles" .}}
<style>
.mail-body-iframe {
width: 100%;
min-height: 300px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #fff;
}
</style>
</head>
<body>
{{template "navbar" .}}
@@ -35,7 +44,7 @@
</div>
<div class="mail-body">
{{if .message.HtmlBody}}
{{.message.HtmlBody | safeHTML}}
<iframe class="mail-body-iframe" srcdoc="{{.message.HtmlBody | safeJS}}" sandbox="allow-same-origin" onload="this.style.height=this.contentDocument.body.scrollHeight+20+'px'"></iframe>
{{else}}
<pre style="white-space:pre-wrap;font-family:inherit;">{{.message.TextBody}}</pre>
{{end}}
+10 -1
View File
@@ -6,6 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看邮件 - MailGo</title>
{{template "styles" .}}
<style>
.mail-body-iframe {
width: 100%;
min-height: 300px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #fff;
}
</style>
</head>
<body>
{{template "navbar" .}}
@@ -32,7 +41,7 @@
</div>
<div class="mail-body">
{{if .message.HtmlBody}}
{{.message.HtmlBody | safeHTML}}
<iframe class="mail-body-iframe" srcdoc="{{.message.HtmlBody | safeJS}}" sandbox="allow-same-origin" onload="this.style.height=this.contentDocument.body.scrollHeight+20+'px'"></iframe>
{{else}}
<pre style="white-space:pre-wrap;font-family:inherit;">{{.message.TextBody}}</pre>
{{end}}