feature/v2.8.6 (#304)

imap协议完善
smtp信息泄露修复
This commit is contained in:
Jinnrry
2025-08-30 20:25:11 +08:00
committed by GitHub
parent 788cd43c43
commit 778e279b4f
9 changed files with 229 additions and 57 deletions

3
docs/debug.md Normal file
View File

@@ -0,0 +1,3 @@
测试imap协议返回
`openssl s_client -crlf -connect imap.xxxx.com:993`

View File

@@ -65,9 +65,9 @@ func Init(version string) error {
}
}
if config.Instance.LogLevel == "debug" {
Instance.ShowSQL(true)
}
//if config.Instance.LogLevel == "debug" {
// Instance.ShowSQL(true)
//}
return nil
}

View File

@@ -2,6 +2,7 @@ package parsemail
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/Jinnrry/pmail/config"
@@ -10,9 +11,9 @@ import (
"github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail"
"github.com/microcosm-cc/bluemonday"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
"github.com/microcosm-cc/bluemonday"
"io"
"mime"
"net/textproto"
@@ -71,7 +72,7 @@ type Email struct {
// Xss filter policy
var (
strictPolicy *bluemonday.Policy
strictPolicy *bluemonday.Policy
relaxedPolicy *bluemonday.Policy
)
@@ -141,10 +142,9 @@ func sanitizeHTML(htmlContent string) string {
return sanitized
}
// Sanitize Text
func sanitizeText(text string) string {
return strictPolicy.Sanitize(text)
return strictPolicy.Sanitize(text)
}
func users2String(users []*User) string {
@@ -360,7 +360,6 @@ func buildUser(str string) *User {
return user
}
func buildUsers(strs []string) []*User {
var ret []*User
for _, line := range strs {
@@ -461,6 +460,33 @@ func (e *Email) ForwardBuildBytes(ctx *context.Context, sender *models.User) []b
return instance.Sign(b.String())
}
func (e *Email) BuildPart(ctx *context.Context, loc []int) []byte {
if len(loc) < 2 {
return nil
}
if loc[0] == 1 && loc[1] == 2 {
encoded := base64.StdEncoding.EncodeToString(e.HTML)
encoded += "\r\n"
return []byte(encoded)
}
if loc[0] == 1 && loc[1] == 1 {
if len(e.Text) == 0 {
encoded := base64.StdEncoding.EncodeToString(e.HTML)
encoded += "\r\n"
return []byte(encoded)
}
encoded := base64.StdEncoding.EncodeToString(e.Text)
encoded += "\r\n"
return []byte(encoded)
}
return nil
}
func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
var b bytes.Buffer
@@ -513,24 +539,27 @@ func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
if err != nil {
log.WithContext(ctx).Fatal(err)
}
var th mail.InlineHeader
th.Header.Set("Content-Transfer-Encoding", "base64")
th.SetContentType("text/plain", map[string]string{
"charset": "UTF-8",
})
w, err := tw.CreatePart(th)
if err != nil {
log.Fatal(err)
if len(e.Text) > 0 {
var th mail.InlineHeader
th.Header.Set("Content-Transfer-Encoding", "base64")
th.SetContentType("text/plain", map[string]string{
"charset": "UTF-8",
})
w, err := tw.CreatePart(th)
if err != nil {
log.Fatal(err)
}
io.WriteString(w, string(e.Text))
w.Close()
}
io.WriteString(w, string(e.Text))
w.Close()
var html mail.InlineHeader
html.SetContentType("text/html", map[string]string{
"charset": "UTF-8",
})
html.Header.Set("Content-Transfer-Encoding", "base64")
w, err = tw.CreatePart(html)
w, err := tw.CreatePart(html)
if err != nil {
log.Fatal(err)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/emersion/go-message"
"io"
"testing"
)
@@ -84,3 +85,13 @@ func TestEmail_builder(t *testing.T) {
rest := e.BuildBytes(nil, false)
fmt.Println(string(rest))
}
func TestEmail_BuildPart(t *testing.T) {
e := Email{
Text: []byte("text"),
HTML: []byte("html"),
}
res := e.BuildPart(nil, []int{1, 2})
fmt.Println(string(res))
}

View File

@@ -17,6 +17,7 @@ require (
github.com/go-acme/lego/v4 v4.23.1
github.com/go-sql-driver/mysql v1.9.2
github.com/lib/pq v1.10.9
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mileusna/spf v0.9.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.7.1
@@ -39,7 +40,6 @@ require (
github.com/gorilla/css v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/miekg/dns v1.1.65 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

View File

@@ -42,14 +42,131 @@ func (s *serverSession) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, opt
return nil
}
// 纯文本 text/plain
func bsTextPlain(size uint32, numLines int64) *imap.BodyStructureSinglePart {
return &imap.BodyStructureSinglePart{
Type: "text",
Subtype: "plain",
Params: map[string]string{"charset": "utf-8"},
Encoding: "base64",
Size: size, // 按字节数
Text: &imap.BodyStructureText{NumLines: numLines},
Extended: &imap.BodyStructureSinglePartExt{},
}
}
// HTML text/html
func bsTextHTML(size uint32, numLines int64) *imap.BodyStructureSinglePart {
return &imap.BodyStructureSinglePart{
Type: "text",
Subtype: "html",
Params: map[string]string{"charset": "utf-8"},
Encoding: "base64",
Size: size,
Text: &imap.BodyStructureText{NumLines: numLines},
Extended: &imap.BodyStructureSinglePartExt{},
}
}
// 通用附件(传入 MIME如 "application/pdf"
func bsAttachment(filename, mime string, size uint32, encoding string) *imap.BodyStructureSinglePart {
mt, st := "application", "octet-stream"
if slash := strings.IndexByte(mime, '/'); slash > 0 {
mt, st = mime[:slash], mime[slash+1:]
}
return &imap.BodyStructureSinglePart{
Type: mt,
Subtype: st,
Params: map[string]string{"name": filename}, // 备用名
ID: "", // 可填 Content-ID
Encoding: encoding, // 常见 "base64"
Size: size,
Extended: &imap.BodyStructureSinglePartExt{
Disposition: &imap.BodyStructureDisposition{
Value: "attachment",
Params: map[string]string{"filename": filename}, // 客户端优先用这里
},
},
}
}
// multipart/alternativetext + html
func bsAlternative(text, html *imap.BodyStructureSinglePart) *imap.BodyStructureMultiPart {
if text == nil && html == nil {
return &imap.BodyStructureMultiPart{
Subtype: "alternative",
Children: []imap.BodyStructure{},
Extended: &imap.BodyStructureMultiPartExt{},
}
}
if text == nil {
return &imap.BodyStructureMultiPart{
Subtype: "alternative",
Children: []imap.BodyStructure{html},
Extended: &imap.BodyStructureMultiPartExt{}, // 可选Params/Disposition/Language/Location
}
}
if html == nil {
return &imap.BodyStructureMultiPart{
Subtype: "alternative",
Children: []imap.BodyStructure{text},
Extended: &imap.BodyStructureMultiPartExt{}, // 可选Params/Disposition/Language/Location
}
}
return &imap.BodyStructureMultiPart{
Subtype: "alternative",
Children: []imap.BodyStructure{text, html},
Extended: &imap.BodyStructureMultiPartExt{}, // 可选Params/Disposition/Language/Location
}
}
// multipart/mixed{ alternative(text+html), attachments... }
func bsMixedWithAttachments(alt *imap.BodyStructureMultiPart, extend bool, atts ...imap.BodyStructure) *imap.BodyStructureMultiPart {
children := []imap.BodyStructure{alt}
children = append(children, atts...)
var ext *imap.BodyStructureMultiPartExt
if extend {
ext = &imap.BodyStructureMultiPartExt{}
}
return &imap.BodyStructureMultiPart{
Subtype: "mixed",
Children: children,
Extended: ext,
}
}
func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*response.EmailResponseData, options *imap.FetchOptions) {
for _, email := range emailList {
writer := w.CreateMessage(cast.ToUint32(email.SerialNumber))
traEmail := parsemail.NewEmailFromModel(email.Email)
if options.UID {
writer.WriteUID(imap.UID(email.UeId))
}
if options.BodyStructure != nil {
var html, text *imap.BodyStructureSinglePart
if len(traEmail.HTML) > 0 {
html = bsTextHTML(uint32(len(traEmail.HTML)), int64(bytes.Count(traEmail.HTML, []byte("\n"))+1))
}
if len(traEmail.Text) > 0 {
text = bsTextPlain(uint32(len(traEmail.Text)), int64(bytes.Count(traEmail.Text, []byte("\n"))+1))
}
alt := bsAlternative(text, html)
var attrs []imap.BodyStructure
for _, attachment := range traEmail.Attachments {
attrs = append(attrs, bsAttachment(attachment.Filename, attachment.ContentType, uint32(len(attachment.Content)), "base64"))
}
bs := bsMixedWithAttachments(alt, options.BodyStructure.Extended, attrs...) // 最终的 BodyStructure接口值
writer.WriteBodyStructure(bs)
}
if options.RFC822Size {
emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false)
emailContent := traEmail.BuildBytes(ctx, false)
writer.WriteRFC822Size(cast.ToInt64(len(emailContent)))
}
if options.Flags {
@@ -66,16 +183,22 @@ func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*respons
if !section.Peek {
detail.MakeRead(ctx, email.Id, true)
}
emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false)
emailContent := traEmail.BuildBytes(ctx, false)
if section.Specifier == imap.PartSpecifierNone || section.Specifier == imap.PartSpecifierText {
bodyWriter := writer.WriteBodySection(section, cast.ToInt64(len(emailContent)))
bodyWriter.Write(emailContent)
bodyWriter.Close()
if len(section.Part) == 2 {
// 取text部分
bodyWriter := writer.WriteBodySection(section, cast.ToInt64(len(emailContent)))
bodyWriter.Write(traEmail.BuildPart(ctx, section.Part))
bodyWriter.Close()
} else {
bodyWriter := writer.WriteBodySection(section, cast.ToInt64(len(emailContent)))
bodyWriter.Write(emailContent)
bodyWriter.Close()
}
}
if section.Specifier == imap.PartSpecifierHeader {
var b bytes.Buffer
parseEmail := parsemail.NewEmailFromModel(email.Email)
fields := section.HeaderFields
if fields == nil || len(fields) == 0 {
@@ -97,10 +220,10 @@ func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*respons
fmt.Fprintf(&b, "From: %s\r\n", email.FromAddress)
}
case "to":
fmt.Fprintf(&b, "To: %s\r\n", parseEmail.BuildTo2String())
fmt.Fprintf(&b, "To: %s\r\n", traEmail.BuildTo2String())
case "cc":
if len(parseEmail.Cc) > 0 {
fmt.Fprintf(&b, "Cc: %s\r\n", parseEmail.BuildCc2String())
if len(traEmail.Cc) > 0 {
fmt.Fprintf(&b, "Cc: %s\r\n", traEmail.BuildCc2String())
}
case "message-id":
fmt.Fprintf(&b, "Message-ID: %s\r\n", fmt.Sprintf("%d@%s", email.Id, config.Instance.Domain))

View File

@@ -11,12 +11,18 @@ import (
func (s *serverSession) Search(kind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
retList := []*response.UserEmailUIDData{}
for _, uidSet := range criteria.UID {
for _, uid := range uidSet {
res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uint32(uid.Start)), cast.ToInt(uint32(uid.Stop)), nil)
retList = append(retList, res...)
if len(criteria.UID) > 0 {
for _, uidSet := range criteria.UID {
for _, uid := range uidSet {
res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uint32(uid.Start)), cast.ToInt(uint32(uid.Stop)), nil)
retList = append(retList, res...)
}
}
} else {
res := list.GetUEListByUID(s.ctx, s.currentMailbox, 0, 0, nil)
retList = append(retList, res...)
}
ret := &imap.SearchData{}
if kind == imapserver.NumKindSeq {

View File

@@ -2,10 +2,10 @@ package smtp_server
import (
"database/sql"
"errors"
"github.com/Jinnrry/pmail/db"
"github.com/Jinnrry/pmail/models"
"github.com/Jinnrry/pmail/utils/context"
"github.com/Jinnrry/pmail/utils/errors"
"github.com/Jinnrry/pmail/utils/id"
"github.com/Jinnrry/pmail/utils/password"
"github.com/emersion/go-sasl"

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"database/sql"
"encoding/json"
oerrors "errors"
"github.com/Jinnrry/pmail/config"
"github.com/Jinnrry/pmail/db"
"github.com/Jinnrry/pmail/dto/parsemail"
@@ -15,7 +16,6 @@ import (
"github.com/Jinnrry/pmail/utils/array"
"github.com/Jinnrry/pmail/utils/async"
"github.com/Jinnrry/pmail/utils/context"
"github.com/Jinnrry/pmail/utils/errors"
"github.com/Jinnrry/pmail/utils/send"
"github.com/mileusna/spf"
log "github.com/sirupsen/logrus"
@@ -69,7 +69,7 @@ func (s *Session) Data(r io.Reader) error {
if s.Ctx.UserID > 0 {
account, _ := email.From.GetDomainAccount()
if account != ctx.UserAccount && !ctx.IsAdmin {
return errors.New("No Auth")
return oerrors.New("No Auth")
}
log.WithContext(ctx).Debugf("开始执行插件SendBefore")