Smail/server/services/rule/rule.go
smail 9b19268c00
Some checks are pending
Security Checks / security (push) Waiting to run
继续收敛邮件策略读取运行时快照
2026-08-31 00:34:30 +08:00

213 lines
6.2 KiB
Go

package rule
import (
"fmt"
"strings"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/db"
"git.pcux.cn/xuans/Smail/dto"
"git.pcux.cn/xuans/Smail/dto/parsemail"
"git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/services/del_email"
"git.pcux.cn/xuans/Smail/services/group"
"git.pcux.cn/xuans/Smail/services/rule/match"
"git.pcux.cn/xuans/Smail/utils/context"
"git.pcux.cn/xuans/Smail/utils/send"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
)
func GetAllRules(ctx *context.Context, userId int) ([]*dto.Rule, error) {
if userId <= 0 {
return nil, fmt.Errorf("invalid user id")
}
var storedRules []*models.Rule
if err := db.Instance.Where("user_id=?", userId).Decr("sort").Find(&storedRules); err != nil {
return nil, fmt.Errorf("load rules: %w", err)
}
ret := make([]*dto.Rule, 0, len(storedRules))
for _, storedRule := range storedRules {
decoded, err := (&dto.Rule{}).Decode(storedRule)
if err != nil {
return nil, fmt.Errorf("decode rule %d: %w", storedRule.Id, err)
}
ret = append(ret, decoded)
}
return ret, nil
}
func MatchRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) bool {
if rule == nil || email == nil || len(rule.Rules) == 0 || ruleContextDone(ctx) {
return false
}
for _, r := range rule.Rules {
if ruleContextDone(ctx) || r == nil {
return false
}
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 {
return false
}
if !m.Match(ctx, email) || ruleContextDone(ctx) {
return false
}
}
return true
}
func ruleContextDone(ctx *context.Context) bool {
return ctx != nil && ctx.Context != nil && ctx.Err() != nil
}
func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email, user *models.User, rawEmailData []byte) error {
if ctx == nil || rule == nil || email == nil || user == nil || db.Instance == nil {
return fmt.Errorf("invalid rule execution context")
}
if rule.UserId <= 0 || user.ID <= 0 || rule.UserId != user.ID {
return fmt.Errorf("rule owner does not match execution user")
}
executionContext := *ctx
executionContext.UserID = user.ID
ctx = &executionContext
log.WithContext(ctx).Debugf("执行规则:%s", rule.Name)
switch rule.Action {
case dto.READ:
if email.MessageId <= 0 {
return fmt.Errorf("email has not been saved")
}
var relation models.UserEmail
found, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", email.MessageId, user.ID).Get(&relation)
if err != nil {
return fmt.Errorf("load rule message relation: %w", err)
}
if !found {
return fmt.Errorf("rule message relation not found")
}
if relation.IsRead == 1 {
return nil
}
affected, err := db.Instance.Table(&models.UserEmail{}).Where("email_id=? and user_id=? and is_read=0", email.MessageId, user.ID).Cols("is_read").Update(map[string]interface{}{"is_read": 1})
if err != nil {
return fmt.Errorf("mark rule message as read: %w", err)
}
if affected != 1 {
return fmt.Errorf("mark rule message as read affected %d rows", affected)
}
case dto.DELETE:
if email.MessageId <= 0 {
return fmt.Errorf("email has not been saved")
}
if err := del_email.DelEmail(ctx, []int{cast.ToInt(email.MessageId)}, false); err != nil {
return fmt.Errorf("delete rule message: %w", err)
}
case dto.FORWARD:
if err := doForward(ctx, email, rule.Params, user, rawEmailData); err != nil {
return fmt.Errorf("forward rule message: %w", err)
}
case dto.MOVE:
if err := doMove(ctx, rule, email); err != nil {
return fmt.Errorf("move rule message: %w", err)
}
default:
return fmt.Errorf("unsupported rule action")
}
return nil
}
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 {
domains := config.CurrentMailDomains()
for _, localDomain := range domains.Accepted {
if strings.EqualFold(domain, localDomain) {
return true
}
}
return strings.EqualFold(domain, domains.Primary)
}
func doMove(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) error {
if rule == nil || email == nil || email.MessageId <= 0 {
return fmt.Errorf("invalid move rule context")
}
groupID := cast.ToInt(rule.Params)
if name, ok := models.GroupCodeToName[groupID]; ok {
return group.Move2DefaultBox(ctx, []int{cast.ToInt(email.MessageId)}, name)
}
return group.MoveMailToGroup(ctx, []int{cast.ToInt(email.MessageId)}, groupID)
}