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" { //if config.Instance.LogLevel == "debug" {
Instance.ShowSQL(true) // Instance.ShowSQL(true)
} //}
return nil return nil
} }

View File

@@ -2,6 +2,7 @@ package parsemail
import ( import (
"bytes" "bytes"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/Jinnrry/pmail/config" "github.com/Jinnrry/pmail/config"
@@ -10,9 +11,9 @@ import (
"github.com/emersion/go-message" "github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset" _ "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail" "github.com/emersion/go-message/mail"
"github.com/microcosm-cc/bluemonday"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/microcosm-cc/bluemonday"
"io" "io"
"mime" "mime"
"net/textproto" "net/textproto"
@@ -141,7 +142,6 @@ func sanitizeHTML(htmlContent string) string {
return sanitized return sanitized
} }
// Sanitize Text // Sanitize Text
func sanitizeText(text string) string { func sanitizeText(text string) string {
return strictPolicy.Sanitize(text) return strictPolicy.Sanitize(text)
@@ -360,7 +360,6 @@ func buildUser(str string) *User {
return user return user
} }
func buildUsers(strs []string) []*User { func buildUsers(strs []string) []*User {
var ret []*User var ret []*User
for _, line := range strs { for _, line := range strs {
@@ -461,6 +460,33 @@ func (e *Email) ForwardBuildBytes(ctx *context.Context, sender *models.User) []b
return instance.Sign(b.String()) 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 { func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
var b bytes.Buffer var b bytes.Buffer
@@ -513,6 +539,8 @@ func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
if err != nil { if err != nil {
log.WithContext(ctx).Fatal(err) log.WithContext(ctx).Fatal(err)
} }
if len(e.Text) > 0 {
var th mail.InlineHeader var th mail.InlineHeader
th.Header.Set("Content-Transfer-Encoding", "base64") th.Header.Set("Content-Transfer-Encoding", "base64")
th.SetContentType("text/plain", map[string]string{ th.SetContentType("text/plain", map[string]string{
@@ -524,13 +552,14 @@ func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
} }
io.WriteString(w, string(e.Text)) io.WriteString(w, string(e.Text))
w.Close() w.Close()
}
var html mail.InlineHeader var html mail.InlineHeader
html.SetContentType("text/html", map[string]string{ html.SetContentType("text/html", map[string]string{
"charset": "UTF-8", "charset": "UTF-8",
}) })
html.Header.Set("Content-Transfer-Encoding", "base64") html.Header.Set("Content-Transfer-Encoding", "base64")
w, err = tw.CreatePart(html) w, err := tw.CreatePart(html)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/emersion/go-message" "github.com/emersion/go-message"
"io" "io"
"testing" "testing"
) )
@@ -84,3 +85,13 @@ func TestEmail_builder(t *testing.T) {
rest := e.BuildBytes(nil, false) rest := e.BuildBytes(nil, false)
fmt.Println(string(rest)) 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-acme/lego/v4 v4.23.1
github.com/go-sql-driver/mysql v1.9.2 github.com/go-sql-driver/mysql v1.9.2
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mileusna/spf v0.9.5 github.com/mileusna/spf v0.9.5
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.7.1 github.com/spf13/cast v1.7.1
@@ -39,7 +40,6 @@ require (
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/miekg/dns v1.1.65 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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 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) { func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*response.EmailResponseData, options *imap.FetchOptions) {
for _, email := range emailList { for _, email := range emailList {
writer := w.CreateMessage(cast.ToUint32(email.SerialNumber)) writer := w.CreateMessage(cast.ToUint32(email.SerialNumber))
traEmail := parsemail.NewEmailFromModel(email.Email)
if options.UID { if options.UID {
writer.WriteUID(imap.UID(email.UeId)) 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 { if options.RFC822Size {
emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false) emailContent := traEmail.BuildBytes(ctx, false)
writer.WriteRFC822Size(cast.ToInt64(len(emailContent))) writer.WriteRFC822Size(cast.ToInt64(len(emailContent)))
} }
if options.Flags { if options.Flags {
@@ -66,16 +183,22 @@ func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*respons
if !section.Peek { if !section.Peek {
detail.MakeRead(ctx, email.Id, true) 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 { if section.Specifier == imap.PartSpecifierNone || section.Specifier == imap.PartSpecifierText {
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 := writer.WriteBodySection(section, cast.ToInt64(len(emailContent)))
bodyWriter.Write(emailContent) bodyWriter.Write(emailContent)
bodyWriter.Close() bodyWriter.Close()
} }
}
if section.Specifier == imap.PartSpecifierHeader { if section.Specifier == imap.PartSpecifierHeader {
var b bytes.Buffer var b bytes.Buffer
parseEmail := parsemail.NewEmailFromModel(email.Email)
fields := section.HeaderFields fields := section.HeaderFields
if fields == nil || len(fields) == 0 { 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) fmt.Fprintf(&b, "From: %s\r\n", email.FromAddress)
} }
case "to": case "to":
fmt.Fprintf(&b, "To: %s\r\n", parseEmail.BuildTo2String()) fmt.Fprintf(&b, "To: %s\r\n", traEmail.BuildTo2String())
case "cc": case "cc":
if len(parseEmail.Cc) > 0 { if len(traEmail.Cc) > 0 {
fmt.Fprintf(&b, "Cc: %s\r\n", parseEmail.BuildCc2String()) fmt.Fprintf(&b, "Cc: %s\r\n", traEmail.BuildCc2String())
} }
case "message-id": case "message-id":
fmt.Fprintf(&b, "Message-ID: %s\r\n", fmt.Sprintf("%d@%s", email.Id, config.Instance.Domain)) 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) { func (s *serverSession) Search(kind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
retList := []*response.UserEmailUIDData{} retList := []*response.UserEmailUIDData{}
if len(criteria.UID) > 0 {
for _, uidSet := range criteria.UID { for _, uidSet := range criteria.UID {
for _, uid := range uidSet { for _, uid := range uidSet {
res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uint32(uid.Start)), cast.ToInt(uint32(uid.Stop)), nil) res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uint32(uid.Start)), cast.ToInt(uint32(uid.Stop)), nil)
retList = append(retList, res...) retList = append(retList, res...)
} }
} }
} else {
res := list.GetUEListByUID(s.ctx, s.currentMailbox, 0, 0, nil)
retList = append(retList, res...)
}
ret := &imap.SearchData{} ret := &imap.SearchData{}
if kind == imapserver.NumKindSeq { if kind == imapserver.NumKindSeq {

View File

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

View File

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