This commit is contained in:
parent
ed7b960921
commit
6254217972
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = "[]"
|
||||
}
|
||||
|
||||
@ -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{}).
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
待投递
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user