Smail/server/services/rule/rule.go
Cd1s 05bca5d5a8
改进邮件转发的投递兼容性 (#347)
优先使用原始邮件数据转发,保留原始发件人、主题、正文、MIME 和 DKIM 签名,避免重建邮件导致严格 DMARC 收件方拒收。

当原始转发被拒时,回退为使用本地转发账号重新投递,同时保持原始主题不变。

同时修正本地域名转发逻辑:精确识别本地域名,自转发时报错,转发给其他本地用户时直接投递到本地收件箱,避免旧的字符串包含判断误拦截。

测试:go test -v ./services/rule ./dto/parsemail ./utils/send

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:29:39 +08:00

224 lines
6.8 KiB
Go

package rule
import (
"fmt"
"strings"
"github.com/Jinnrry/pmail/config"
"github.com/Jinnrry/pmail/consts"
"github.com/Jinnrry/pmail/db"
"github.com/Jinnrry/pmail/dto"
"github.com/Jinnrry/pmail/dto/parsemail"
"github.com/Jinnrry/pmail/models"
"github.com/Jinnrry/pmail/services/rule/match"
"github.com/Jinnrry/pmail/utils/context"
"github.com/Jinnrry/pmail/utils/send"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
)
func GetAllRules(ctx *context.Context, userId int) []*dto.Rule {
var res []*models.Rule
var err error
if userId == 0 {
return nil
} else {
err = db.Instance.Where("user_id=?", userId).Decr("sort").Find(&res)
}
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
var ret []*dto.Rule
for _, rule := range res {
ret = append(ret, (&dto.Rule{}).Decode(rule))
}
return ret
}
func MatchRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) bool {
for _, r := range rule.Rules {
var m match.Match
switch r.Type {
case match.RuleTypeRegex:
m = match.NewRegexMatch(r.Field, r.Rule)
case match.RuleTypeContains:
m = match.NewContainsMatch(r.Field, r.Rule)
case match.RuleTypeEq:
m = match.NewEqualMatch(r.Field, r.Rule)
}
if m == nil {
continue
}
if !m.Match(ctx, email) {
return false
}
}
return true
}
func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email, user *models.User, rawEmailData []byte) {
log.WithContext(ctx).Debugf("执行规则:%s", rule.Name)
switch rule.Action {
case dto.READ:
if email.MessageId > 0 {
_, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).Cols("is_read").Update(map[string]interface{}{"is_read": 1})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
}
case dto.DELETE:
_, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).Cols("status").Update(map[string]interface{}{"status": consts.EmailStatusDel})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
case dto.FORWARD:
err := doForward(ctx, email, rule.Params, user, rawEmailData)
if err != nil {
log.WithContext(ctx).Errorf("Forward Error:%v", err)
} else {
log.WithContext(ctx).Infof("Forward Success:%s@%s -> %s", user.Account, config.Instance.Domains[0], rule.Params)
}
case dto.MOVE:
doMove(ctx, rule, email, user)
}
}
func doForward(ctx *context.Context, email *parsemail.Email, forwardAddress string, user *models.User, rawEmailData []byte) error {
forwardUser := parsemail.BuilderUser(forwardAddress)
if forwardUser == nil || forwardUser.EmailAddress == "" {
return fmt.Errorf("invalid forward address: %s", forwardAddress)
}
account, domain := forwardUser.GetDomainAccount()
if account == "" || domain == "" {
return fmt.Errorf("invalid forward address: %s", forwardAddress)
}
if isLocalDomain(domain) {
if strings.EqualFold(account, user.Account) {
return fmt.Errorf("loop forwarding to self: %s", forwardAddress)
}
return forwardToLocalUser(ctx, email, account, forwardAddress)
}
if len(rawEmailData) > 0 {
err := send.ForwardRaw(ctx, email, rawEmailData, forwardAddress, user)
if err == nil {
return nil
}
log.WithContext(ctx).Warnf("Raw forward failed, retry remail forward:%v", err)
}
return send.Forward(ctx, email, forwardAddress, user)
}
func forwardToLocalUser(ctx *context.Context, email *parsemail.Email, account, forwardAddress string) error {
if email.MessageId <= 0 {
return fmt.Errorf("email has not been saved before local forwarding")
}
var user models.User
has, err := db.Instance.Table(&models.User{}).
Where("LOWER(account)=LOWER(?) and disabled=0", account).
Get(&user)
if err != nil {
return err
}
if !has {
return fmt.Errorf("local forward user not found: %s", forwardAddress)
}
var ue models.UserEmail
has, err = db.Instance.Table(&models.UserEmail{}).
Where("email_id=? and user_id=?", email.MessageId, user.ID).
Get(&ue)
if err != nil {
return err
}
if has {
return nil
}
_, err = db.Instance.Insert(&models.UserEmail{
UserID: user.ID,
EmailID: cast.ToInt(email.MessageId),
Status: cast.ToInt8(email.Status),
})
return err
}
func isLocalDomain(domain string) bool {
for _, localDomain := range config.Instance.Domains {
if strings.EqualFold(domain, localDomain) {
return true
}
}
return strings.EqualFold(domain, config.Instance.Domain)
}
func doMove(ctx *context.Context, rule *dto.Rule, email *parsemail.Email, user *models.User) {
groupId := cast.ToInt(rule.Params)
switch groupId {
case models.INBOX:
_, err := db.Instance.Table(&models.Email{}).Where("id=?", email.MessageId).
Cols("type").Update(map[string]interface{}{"type": consts.EmailTypeReceive})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
_, err = db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": 0})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
case models.Sent:
_, err := db.Instance.Table(&models.Email{}).Where("id=?", email.MessageId).
Cols("type").Update(map[string]interface{}{"type": consts.EmailTypeSend})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
_, err = db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": 0})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
case models.Drafts:
_, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": consts.EmailStatusDrafts})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
case models.Deleted:
_, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": consts.EmailStatusDel})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
case models.Junk:
_, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).
Cols("group_id", "status").Update(map[string]interface{}{"group_id": 0, "status": consts.EmailStatusJunk})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
default:
_, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, rule.UserId).Cols("group_id").Update(map[string]interface{}{"group_id": groupId})
if err != nil {
log.WithContext(ctx).Errorf("sqlERror :%v", err)
}
}
}