Some checks are pending
Security Checks / security (push) Waiting to run
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
213 lines
6.2 KiB
Go
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)
|
|
}
|