From 6254217972323d057ead9dda5773766dd1b03e63 Mon Sep 17 00:00:00 2001 From: smail Date: Fri, 25 Sep 2026 01:44:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8C=89=E6=94=B6=E4=BB=B6?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=8B=A6=E6=88=AA=E9=BB=91=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=B9=B6=E8=A1=A5=E9=BD=90=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E8=AD=A6=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/email/list.go | 8 +++- backend/dto/parsemail/email.go | 15 ++++++++ backend/dto/response/email.go | 13 ++++--- backend/listen/smtp_server/read_content.go | 28 ++++++++++++++ backend/models/user_contact.go | 38 ++++++++++++++++++- backend/services/detail/detail.go | 13 +++++++ backend/services/rule/rule.go | 9 +++++ .../src/views/EmailDetail/EmailDetail.vue | 8 ++++ frontend/src/views/List/List.vue | 5 +++ .../SenderFilterManagement.vue | 9 ++++- 10 files changed, 136 insertions(+), 10 deletions(-) diff --git a/backend/controllers/email/list.go b/backend/controllers/email/list.go index 7762038..8dce886 100644 --- a/backend/controllers/email/list.go +++ b/backend/controllers/email/list.go @@ -89,7 +89,11 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) { whitelistMap := make(map[string]bool) blacklistMap := make(map[string]bool) var senderFilters []models.UserTrustedSender - if err := db.Instance.Table(&models.UserTrustedSender{}).Where("user_id = ?", ctx.UserID).Find(&senderFilters); err == nil { + if err := db.Instance.Table(&models.UserTrustedSender{}).Where("user_id = ?", ctx.UserID).Find(&senderFilters); err != nil { + log.WithContext(ctx).WithError(err).Error("load sender filters failed") + response.NewErrorResponse(response.ServerError, "读取发件人安全标记失败", "").FPrint(w) + return + } else { for _, item := range senderFilters { addr := strings.ToLower(strings.TrimSpace(item.Email)) if item.FilterType == models.FilterTypeBlacklist { @@ -122,7 +126,7 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) { authentication := parsemail.NewEmailAuthentication(email.SPFCheck == 1, email.DKIMCheck == 1) senderAddr := strings.ToLower(strings.TrimSpace(sender.EmailAddress)) isTrusted := whitelistMap[senderAddr] - isBlacklisted := blacklistMap[senderAddr] + isBlacklisted := blacklistMap[senderAddr] || blacklistMap[strings.ToLower(strings.TrimSpace(email.FromAddress))] dangerous := authentication.Dangerous if isBlacklisted { dangerous = true diff --git a/backend/dto/parsemail/email.go b/backend/dto/parsemail/email.go index bee160a..a0b500a 100644 --- a/backend/dto/parsemail/email.go +++ b/backend/dto/parsemail/email.go @@ -73,6 +73,7 @@ func NewEmailAuthentication(spfPassed, dkimPassed bool) *EmailAuthentication { // Email is the type used for email messages type Email struct { + EnvelopeFrom string ReplyTo []*User From *User To []*User @@ -93,6 +94,20 @@ type Email struct { Authentication *EmailAuthentication } +func (e *Email) SenderFilterAddresses() []string { + if e == nil { + return nil + } + addresses := []string{e.EnvelopeFrom} + if e.From != nil { + addresses = append(addresses, e.From.EmailAddress) + } + if e.Sender != nil { + addresses = append(addresses, e.Sender.EmailAddress) + } + return addresses +} + type StoredEmailFields struct { ReplyTo string To string diff --git a/backend/dto/response/email.go b/backend/dto/response/email.go index ca95c77..51571e3 100644 --- a/backend/dto/response/email.go +++ b/backend/dto/response/email.go @@ -3,12 +3,13 @@ package response import "git.pcux.cn/xuans/Smail/models" type EmailResponseData struct { - models.Email `xorm:"extends"` - IsRead int8 `json:"is_read"` - IsDeleted int8 `json:"-"` - UserStatus int8 `xorm:"user_status" json:"-"` - SerialNumber int `json:"serial_number"` - UeId int `json:"ue_id"` + models.Email `xorm:"extends"` + IsRead int8 `json:"is_read"` + IsDeleted int8 `json:"-"` + UserStatus int8 `xorm:"user_status" json:"-"` + SerialNumber int `json:"serial_number"` + UeId int `json:"ue_id"` + IsBlacklisted bool `xorm:"-" json:"is_blacklisted"` } type UserEmailUIDData struct { diff --git a/backend/listen/smtp_server/read_content.go b/backend/listen/smtp_server/read_content.go index f14d766..504e3dd 100644 --- a/backend/listen/smtp_server/read_content.go +++ b/backend/listen/smtp_server/read_content.go @@ -40,6 +40,8 @@ import ( // 注意:此开关只能通过修改代码来更改,不暴露给用户配置 const DropUnknownRecipientEmails = true +var errAllRecipientsBlocked = oerrors.New("all recipients blocked by sender policy") + func (s *Session) Data(r io.Reader) (resultErr error) { if err := s.checkAuthorization(); err != nil { return err @@ -118,6 +120,7 @@ func (s *Session) Data(r io.Reader) (resultErr error) { return &smtp.SMTPError{Code: 550, EnhancedCode: smtp.EnhancedCode{5, 6, 0}, Message: "Invalid message content"} } + email.EnvelopeFrom = s.From if s.From != "" { from := parsemail.BuilderUser(s.From) if email.From == nil { @@ -200,6 +203,9 @@ func (s *Session) Data(r io.Reader) (resultErr error) { details.Stage = "保存收到的邮件" users, dbEmail, saveErr := saveEmail(ctx, len(emailData), email, 0, 0, s.To, SPFStatus, dkimStatus) + if oerrors.Is(saveErr, errAllRecipientsBlocked) { + return &smtp.SMTPError{Code: 550, EnhancedCode: smtp.EnhancedCode{5, 7, 1}, Message: "Message rejected by recipient policy"} + } if saveErr != nil { log.WithContext(ctx).Errorf("CRITICAL audit_event=initial_persist_failed direction=inbound msg_id=%q error=%v", email.MsgID, saveErr) return localPersistenceSMTPError() @@ -325,6 +331,28 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI if err != nil || drop { return users, nil, err } + if emailType == int(consts.EmailTypeReceive) { + userIDs := make([]int, 0, len(users)) + for _, user := range users { + userIDs = append(userIDs, user.ID) + } + session := db.Instance.NewSession() + blocked, filterErr := models.BlacklistedRecipientIDs(session, userIDs, email.SenderFilterAddresses()...) + session.Close() + if filterErr != nil { + return nil, nil, filterErr + } + allowed := make([]*models.User, 0, len(users)) + for _, user := range users { + if !blocked[user.ID] { + allowed = append(allowed, user) + } + } + users = allowed + if len(users) == 0 { + return nil, nil, errAllRecipientsBlocked + } + } msgID := email.MsgID if msgID == "" { diff --git a/backend/models/user_contact.go b/backend/models/user_contact.go index 786d931..a0aae04 100644 --- a/backend/models/user_contact.go +++ b/backend/models/user_contact.go @@ -1,6 +1,13 @@ package models -import "time" +import ( + "fmt" + "strings" + "time" + + "xorm.io/builder" + "xorm.io/xorm" +) // UserContact 保存单个用户的厇史写信收仴人,用于写信页自动补全与遗系人管理 type UserContact struct { @@ -29,3 +36,32 @@ type UserTrustedSender struct { } func (UserTrustedSender) TableName() string { return "user_trusted_sender" } + +func BlacklistedRecipientIDs(session *xorm.Session, userIDs []int, addresses ...string) (map[int]bool, error) { + blocked := make(map[int]bool) + if session == nil { + return nil, fmt.Errorf("sender filter database session is unavailable") + } + var normalized []string + for _, address := range addresses { + if address = strings.ToLower(strings.TrimSpace(address)); address != "" { + normalized = append(normalized, address) + } + } + if len(userIDs) == 0 || len(normalized) == 0 { + return blocked, nil + } + var filters []UserTrustedSender + err := session.Table(new(UserTrustedSender)).Where(builder.And( + builder.In("user_id", userIDs), + builder.In("LOWER(email)", normalized), + builder.Eq{"filter_type": FilterTypeBlacklist}, + )).Find(&filters) + if err != nil { + return nil, fmt.Errorf("load recipient sender filters: %w", err) + } + for _, filter := range filters { + blocked[filter.UserID] = true + } + return blocked, nil +} diff --git a/backend/services/detail/detail.go b/backend/services/detail/detail.go index 3e8e4c4..f1604be 100644 --- a/backend/services/detail/detail.go +++ b/backend/services/detail/detail.go @@ -42,6 +42,19 @@ func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*response.Emai } email.IsRead = ue.IsRead + var sender parsemail.User + if strings.TrimSpace(email.Sender) != "" { + if err := json.Unmarshal([]byte(email.Sender), &sender); err != nil { + return nil, fmt.Errorf("decode stored sender: %w", err) + } + } + session := db.Instance.NewSession() + blocked, err := models.BlacklistedRecipientIDs(session, []int{ctx.UserID}, email.FromAddress, sender.EmailAddress) + session.Close() + if err != nil { + return nil, err + } + email.IsBlacklisted = blocked[ctx.UserID] if email.Type == 0 { email.Bcc = "[]" } diff --git a/backend/services/rule/rule.go b/backend/services/rule/rule.go index ea8aa60..d7f5c54 100644 --- a/backend/services/rule/rule.go +++ b/backend/services/rule/rule.go @@ -169,6 +169,15 @@ func forwardToLocalUser(ctx *context.Context, email *parsemail.Email, account, f if !has { return fmt.Errorf("local forward user not found: %s", forwardAddress) } + session := db.Instance.NewSession() + blocked, filterErr := models.BlacklistedRecipientIDs(session, []int{user.ID}, email.SenderFilterAddresses()...) + session.Close() + if filterErr != nil { + return filterErr + } + if blocked[user.ID] { + return fmt.Errorf("local forwarding rejected by recipient policy") + } var ue models.UserEmail has, err = db.Instance.Table(&models.UserEmail{}). diff --git a/frontend/src/views/EmailDetail/EmailDetail.vue b/frontend/src/views/EmailDetail/EmailDetail.vue index c628fc3..b0bdd1e 100644 --- a/frontend/src/views/EmailDetail/EmailDetail.vue +++ b/frontend/src/views/EmailDetail/EmailDetail.vue @@ -23,6 +23,14 @@
+

{{ detailData.subject }}

diff --git a/frontend/src/views/List/List.vue b/frontend/src/views/List/List.vue index 8044dd0..8cda742 100644 --- a/frontend/src/views/List/List.vue +++ b/frontend/src/views/List/List.vue @@ -72,12 +72,17 @@
+ + 黑名单发件人 + +