mirror of
https://github.com/Jinnrry/PMail.git
synced 2025-09-27 03:35:56 +08:00
3
docs/debug.md
Normal file
3
docs/debug.md
Normal file
@@ -0,0 +1,3 @@
|
||||
测试imap协议返回
|
||||
|
||||
`openssl s_client -crlf -connect imap.xxxx.com:993`
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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))
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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/alternative:text + 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))
|
||||
|
@@ -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 {
|
||||
|
@@ -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"
|
||||
|
@@ -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!")
|
||||
|
Reference in New Issue
Block a user