Smail/server/dto/parsemail/email.go
Jinnrry e6a56b199d
v2.6.2 (#177)
1、修复邮件插入失败
2、插件支持设置页面
3、修复pop3邮箱拉取权限判断
4、修复非管理员账户的附件权限判断

Co-authored-by: jinnrry <i@jinnrry.com>
2024-07-27 20:47:42 +08:00

375 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package parsemail
import (
"bytes"
"fmt"
"github.com/Jinnrry/pmail/config"
"github.com/Jinnrry/pmail/utils/context"
"github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail"
log "github.com/sirupsen/logrus"
"io"
"net/textproto"
"regexp"
"strings"
"time"
)
type User struct {
EmailAddress string `json:"EmailAddress"`
Name string `json:"Name"`
}
func (u User) GetDomainAccount() (string, string) {
infos := strings.Split(u.EmailAddress, "@")
if len(infos) >= 2 {
return infos[0], infos[1]
}
return "", ""
}
type Attachment struct {
Filename string
ContentType string
Content []byte
ContentID string
}
// 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删除
MessageId int64
Size int
}
func NewEmailFromReader(to []string, r io.Reader, size int) *Email {
ret := &Email{}
m, err := message.Read(r)
if err != nil {
log.Errorf("email解析错误! Error %+v", err)
}
ret.Size = size
ret.From = buildUser(m.Header.Get("From"))
if len(to) > 0 {
ret.To = buildUsers(to)
} else {
ret.To = buildUsers(m.Header.Values("To"))
}
ret.Cc = buildUsers(m.Header.Values("Cc"))
ret.ReplyTo = buildUsers(m.Header.Values("ReplyTo"))
ret.Sender = buildUser(m.Header.Get("Sender"))
if ret.Sender == nil {
ret.Sender = ret.From
}
ret.Subject, _ = m.Header.Text("Subject")
sendTime, err := time.Parse(time.RFC1123Z, m.Header.Get("Date"))
if err != nil {
sendTime = time.Now()
}
ret.Date = sendTime.Format(time.DateTime)
m.Walk(func(path []int, entity *message.Entity, err error) error {
return formatContent(entity, ret)
})
return ret
}
func formatContent(entity *message.Entity, ret *Email) error {
contentType, p, err := entity.Header.ContentType()
if err != nil {
log.Errorf("email read error! %+v", err)
return err
}
switch contentType {
case "multipart/alternative":
case "multipart/mixed":
case "text/plain":
ret.Text, _ = io.ReadAll(entity.Body)
case "text/html":
ret.HTML, _ = io.ReadAll(entity.Body)
case "multipart/related":
entity.Walk(func(path []int, entity *message.Entity, err error) error {
if t, _, _ := entity.Header.ContentType(); t == "multipart/related" {
return nil
}
return formatContent(entity, ret)
})
default:
c, _ := io.ReadAll(entity.Body)
fileName := p["name"]
if fileName == "" {
contentDisposition := entity.Header.Get("Content-Disposition")
r := regexp.MustCompile("filename=(.*)")
matchs := r.FindStringSubmatch(contentDisposition)
if len(matchs) == 2 {
fileName = matchs[1]
} else {
fileName = "no_name_file"
}
}
ret.Attachments = append(ret.Attachments, &Attachment{
Filename: fileName,
ContentType: contentType,
Content: c,
ContentID: strings.TrimPrefix(strings.TrimSuffix(entity.Header.Get("Content-Id"), ">"), "<"),
})
}
return nil
}
func BuilderUser(str string) *User {
return buildUser(str)
}
var emailAddressRe = regexp.MustCompile(`<(.*@.*)>`)
func buildUser(str string) *User {
if str == "" {
return nil
}
ret := &User{}
matched := emailAddressRe.FindStringSubmatch(str)
if len(matched) == 2 {
ret.EmailAddress = matched[1]
} else {
ret.EmailAddress = str
return ret
}
str = strings.ReplaceAll(str, matched[0], "")
str = strings.Trim(strings.TrimSpace(str), "\"")
name, err := (&WordDecoder{}).Decode(strings.ReplaceAll(str, "\"", ""))
if err == nil {
ret.Name = strings.TrimSpace(name)
} else {
ret.Name = strings.TrimSpace(str)
}
return ret
}
func buildUsers(str []string) []*User {
var ret []*User
for _, s1 := range str {
if s1 == "" {
continue
}
for _, s := range strings.Split(s1, ",") {
s = strings.TrimSpace(s)
ret = append(ret, buildUser(s))
}
}
return ret
}
func (e *Email) ForwardBuildBytes(ctx *context.Context, forwardAddress string) []byte {
var b bytes.Buffer
from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
to := []*mail.Address{
{
Address: forwardAddress,
},
}
// Create our mail header
var h mail.Header
h.SetDate(time.Now())
h.SetAddressList("From", from)
h.SetAddressList("To", to)
h.SetText("Subject", e.Subject)
h.SetMessageID(fmt.Sprintf("%d@%s", e.MessageId, config.Instance.Domain))
if len(e.Cc) != 0 {
cc := []*mail.Address{}
for _, user := range e.Cc {
cc = append(cc, &mail.Address{
Name: user.Name,
Address: user.EmailAddress,
})
}
h.SetAddressList("Cc", cc)
}
// Create a new mail writer
mw, err := mail.CreateWriter(&b, h)
if err != nil {
log.WithContext(ctx).Fatal(err)
}
// Create a text part
tw, err := mw.CreateInline()
if err != nil {
log.WithContext(ctx).Fatal(err)
}
var th mail.InlineHeader
th.Set("Content-Type", "text/plain")
w, err := tw.CreatePart(th)
if err != nil {
log.Fatal(err)
}
io.WriteString(w, string(e.Text))
w.Close()
var html mail.InlineHeader
html.Set("Content-Type", "text/html")
w, err = tw.CreatePart(html)
if err != nil {
log.Fatal(err)
}
io.WriteString(w, string(e.HTML))
w.Close()
tw.Close()
// Create an attachment
for _, attachment := range e.Attachments {
var ah mail.AttachmentHeader
ah.Set("Content-Type", attachment.ContentType)
ah.SetFilename(attachment.Filename)
w, err = mw.CreateAttachment(ah)
if err != nil {
log.WithContext(ctx).Fatal(err)
continue
}
w.Write(attachment.Content)
w.Close()
}
mw.Close()
// dkim 签名后返回
return instance.Sign(b.String())
}
func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
var b bytes.Buffer
from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
to := []*mail.Address{}
for _, user := range e.To {
to = append(to, &mail.Address{
Name: user.Name,
Address: user.EmailAddress,
})
}
// Create our mail header
var h mail.Header
if e.Date != "" {
t, err := time.ParseInLocation("2006-01-02 15:04:05", e.Date, time.Local)
if err != nil {
log.WithContext(ctx).Errorf("Time Error ! Err:%+v", err)
h.SetDate(time.Now())
} else {
h.SetDate(t)
}
} else {
h.SetDate(time.Now())
}
h.SetMessageID(fmt.Sprintf("%d@%s", e.MessageId, config.Instance.Domain))
h.SetAddressList("From", from)
h.SetAddressList("To", to)
h.SetText("Subject", e.Subject)
if len(e.Cc) != 0 {
cc := []*mail.Address{}
for _, user := range e.Cc {
cc = append(cc, &mail.Address{
Name: user.Name,
Address: user.EmailAddress,
})
}
h.SetAddressList("Cc", cc)
}
// Create a new mail writer
mw, err := mail.CreateWriter(&b, h)
if err != nil {
log.WithContext(ctx).Fatal(err)
}
// Create a text part
tw, err := mw.CreateInline()
if err != nil {
log.WithContext(ctx).Fatal(err)
}
var th mail.InlineHeader
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()
var html mail.InlineHeader
html.SetContentType("text/html", map[string]string{
"charset": "UTF-8",
})
w, err = tw.CreatePart(html)
if err != nil {
log.Fatal(err)
}
if len(e.HTML) > 0 {
io.WriteString(w, string(e.HTML))
} else {
io.WriteString(w, string(e.Text))
}
w.Close()
tw.Close()
// Create an attachment
for _, attachment := range e.Attachments {
var ah mail.AttachmentHeader
ah.Set("Content-Type", attachment.ContentType)
ah.SetFilename(attachment.Filename)
w, err = mw.CreateAttachment(ah)
if err != nil {
log.WithContext(ctx).Fatal(err)
continue
}
w.Write(attachment.Content)
w.Close()
}
mw.Close()
if dkim {
// dkim 签名后返回
return instance.Sign(b.String())
}
return b.Bytes()
}