修复按收件用户拦截黑名单邮件并补齐历史邮件警示
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-25 01:44:44 +08:00
parent ed7b960921
commit 6254217972
10 changed files with 136 additions and 10 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 == "" {

View File

@ -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
}

View File

@ -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 = "[]"
}

View File

@ -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{}).

View File

@ -23,6 +23,14 @@
</div>
</div>
<div v-else class="mail-detail-content">
<el-alert
v-if="detailData.is_blacklisted"
title="该发件人已在你的黑名单中"
description="这是已保存的历史邮件。黑名单会拦截后续新邮件,不会自动删除历史邮件。请谨慎查看内容与附件。"
type="error"
:closable="false"
show-icon
/>
<h1 class="mail-subject">{{ detailData.subject }}</h1>
<div class="mail-meta-card">

View File

@ -72,12 +72,17 @@
<!-- 高辨识度安全与发送状态标签 -->
<div class="mail-status-tags" v-if="scope.row.dangerous || isOutbox">
<el-tag v-if="scope.row.is_blacklisted" type="danger" effect="dark" size="small">
黑名单发件人
</el-tag>
<template v-else>
<el-tooltip content="点击将该发件人添加至信任白名单,后续免除风险提示" placement="top" v-if="scope.row.dangerous">
<el-tag type="danger" effect="dark" size="small" class="clickable-tag" @click.stop="trustSender(scope.row.sender.EmailAddress)">
<el-icon><Warning /></el-icon> 疑似不安全 (点击信任)
</el-tag>
</el-tooltip>
</template>
<template v-if="isOutbox">
<el-tag type="warning" effect="light" size="small" v-if="scope.row.status === 6">
待投递

View File

@ -1,5 +1,12 @@
<template>
<div class="settings-card sender-filter-card" v-loading="loading">
<el-alert
title="黑名单仅对当前账号生效"
description="命中黑名单的新邮件不会进入你的邮箱,也不会触发你的收信规则;不影响其他收件人。历史邮件保留并显示警告,白名单不会绕过协议校验。"
type="info"
:closable="false"
show-icon
/>
<div class="settings-header sf-header">
<div class="header-main">
<h3>发件人黑白名单</h3>
@ -179,4 +186,4 @@ const {
handleDelete,
formatDate,
} = useSenderFilters()
</script>
</script>