fix: resolve multipart email parsing issue (#336)

* fix: resolve multipart email parsing issue

* resolve multipart email parsing issue

解决邮件正文被附件覆盖的bug

* Update email.go

修改xss过滤
This commit is contained in:
QvaaX 2026-05-22 17:45:06 +08:00 committed by GitHub
parent c347f30c0c
commit b0913ee804
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -54,23 +54,23 @@ type Attachment struct {
// Email is the type used for email messages
type Email struct {
ReplyTo []*User
From *User
To []*User
Bcc []*User
Cc []*User
Subject string
Text []byte // Plaintext message (optional)
HTML []byte // Html message (optional)
Sender *User // override From as SMTP envelope sender (optional)
Headers textproto.MIMEHeader
Attachments []*Attachment
ReadReceipt []string
Date string
Status int // 0未发送,1已发送,2发送失败,3删除,5广告邮件
MessageId int64
MsgID string // RFC-compliant Message-ID, persisted in DB
Size int
ReplyTo []*User
From *User
To []*User
Bcc []*User
Cc []*User
Subject string
Text []byte // Plaintext message (optional)
HTML []byte // Html message (optional)
Sender *User // override From as SMTP envelope sender (optional)
Headers textproto.MIMEHeader
Attachments []*Attachment
ReadReceipt []string
Date string
Status int // 0未发送,1已发送,2发送失败,3删除,5广告邮件
MessageId int64
MsgID string // RFC-compliant Message-ID, persisted in DB
Size int
}
// GenerateMsgID creates an RFC-compliant Message-ID unique enough to avoid spam filters.
@ -289,16 +289,45 @@ func formatContent(entity *message.Entity, ret *Email) error {
log.Errorf("email read error! %+v", err)
return err
}
disp := strings.ToLower(entity.Header.Get("Content-Disposition"))
isAttachment := strings.Contains(disp, "attachment")
switch contentType {
case "multipart/alternative":
case "multipart/mixed":
case "text/plain":
testContent, _ := io.ReadAll(entity.Body)
ret.Text = []byte(strictPolicy.Sanitize(string(testContent)))
body, _ := io.ReadAll(entity.Body)
if isAttachment {
fileName := getFileName(entity, p)
ret.Attachments = append(ret.Attachments, &Attachment{
Filename: sanitizeText(fileName),
ContentType: sanitizeText(strings.TrimSpace(contentType)),
Content: body,
ContentID: strings.TrimPrefix(strings.TrimSuffix(entity.Header.Get("Content-Id"), ">"), "<"),
})
return nil
}
ret.Text = []byte(strictPolicy.Sanitize(string(body)))
case "text/html":
htmlContent, _ := io.ReadAll(entity.Body)
ret.HTML = []byte(relaxedPolicy.Sanitize(string(htmlContent)))
body, _ := io.ReadAll(entity.Body)
if isAttachment {
fileName := getFileName(entity, p)
ret.Attachments = append(ret.Attachments, &Attachment{
Filename: sanitizeText(fileName),
ContentType: sanitizeText(strings.TrimSpace(contentType)),
Content: body,
ContentID: strings.TrimPrefix(strings.TrimSuffix(entity.Header.Get("Content-Id"), ">"), "<"),
})
return nil
}
ret.HTML = []byte(relaxedPolicy.Sanitize(string(body)))
case "multipart/related":
entity.Walk(func(path []int, entity *message.Entity, err error) error {
if t, _, _ := entity.Header.ContentType(); t == "multipart/related" {
@ -308,18 +337,7 @@ func formatContent(entity *message.Entity, ret *Email) error {
})
default:
c, _ := io.ReadAll(entity.Body)
fileName := p["name"]
if fileName == "" {
contentDisposition := entity.Header.Get("Content-Disposition")
filenameRegex := regexp.MustCompile(`filename\s*=\s*"?([^";]+)"?`)
matches := filenameRegex.FindStringSubmatch(contentDisposition)
if len(matches) >= 2 {
fileName = strings.TrimSpace(matches[1])
fileName = strings.Trim(fileName, `"`)
} else {
fileName = "no_name_file"
}
}
fileName := getFileName(entity, p)
ret.Attachments = append(ret.Attachments, &Attachment{
Filename: sanitizeText(fileName),
@ -331,6 +349,24 @@ func formatContent(entity *message.Entity, ret *Email) error {
return nil
}
func getFileName(entity *message.Entity, p map[string]string) string {
fileName := p["name"]
if fileName != "" {
return fileName
}
contentDisposition := entity.Header.Get("Content-Disposition")
filenameRegex := regexp.MustCompile(`filename\s*=\s*"?([^";]+)"?`)
matches := filenameRegex.FindStringSubmatch(contentDisposition)
if len(matches) >= 2 {
return strings.Trim(strings.TrimSpace(matches[1]), `"`)
}
return "no_name_file"
}
func BuilderUser(str string) *User {
return buildUser(str)