feat(filter): 实现发件人黑白名单数据库迁移、控制器与API路由及收件列表联动
Some checks are pending
Security Checks / security (push) Waiting to run
Some checks are pending
Security Checks / security (push) Waiting to run
This commit is contained in:
parent
fcb64a34bd
commit
bbb671c5ab
@ -3,12 +3,14 @@ package email
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/db"
|
||||
"git.pcux.cn/xuans/Smail/dto/response"
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/utils/address"
|
||||
"git.pcux.cn/xuans/Smail/utils/context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"xorm.io/xorm"
|
||||
@ -23,8 +25,23 @@ type trustSenderRequest struct {
|
||||
Trusted bool `json:"trusted"`
|
||||
}
|
||||
|
||||
type senderFilterRequest struct {
|
||||
ID int `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FilterType string `json:"filter_type"`
|
||||
}
|
||||
|
||||
type toggleSenderFilterRequest struct {
|
||||
Email string `json:"email"`
|
||||
TargetType string `json:"target_type"`
|
||||
}
|
||||
|
||||
// ContactsList 查询当前用户的历史写信收件人列表(多租户严格隔离)
|
||||
func ContactsList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
var contacts []models.UserContact
|
||||
err := db.Instance.Table(&models.UserContact{}).
|
||||
Where("user_id = ?", ctx.UserID).
|
||||
@ -42,6 +59,10 @@ func ContactsList(ctx *context.Context, w http.ResponseWriter, req *http.Request
|
||||
|
||||
// DeleteContact 删除当前用户的某项历史联系人记录
|
||||
func DeleteContact(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
var reqData deleteContactRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&reqData); err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
@ -67,6 +88,10 @@ func DeleteContact(ctx *context.Context, w http.ResponseWriter, req *http.Reques
|
||||
|
||||
// TrustSender 将发件人添加至信任白名单,或从中移除
|
||||
func TrustSender(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
var reqData trustSenderRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&reqData); err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
@ -78,22 +103,30 @@ func TrustSender(ctx *context.Context, w http.ResponseWriter, req *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if reqData.Trusted {
|
||||
var exists bool
|
||||
var existing models.UserTrustedSender
|
||||
has, err := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND LOWER(email) = ?", ctx.UserID, email).
|
||||
Exist(&models.UserTrustedSender{})
|
||||
Get(&existing)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("check trusted sender failed")
|
||||
response.NewErrorResponse(response.ServerError, "操作失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
exists = has
|
||||
if !exists {
|
||||
if has {
|
||||
if existing.FilterType != models.FilterTypeWhitelist {
|
||||
existing.FilterType = models.FilterTypeWhitelist
|
||||
existing.UpdatedAt = &now
|
||||
_, _ = db.Instance.ID(existing.ID).Cols("filter_type", "updated_at").Update(&existing)
|
||||
}
|
||||
} else {
|
||||
item := models.UserTrustedSender{
|
||||
UserID: ctx.UserID,
|
||||
Email: email,
|
||||
CreatedAt: time.Now(),
|
||||
FilterType: models.FilterTypeWhitelist,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: &now,
|
||||
}
|
||||
if _, err := db.Instance.Insert(&item); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("insert trusted sender failed")
|
||||
@ -106,7 +139,227 @@ func TrustSender(ctx *context.Context, w http.ResponseWriter, req *http.Request)
|
||||
Where("user_id = ? AND LOWER(email) = ?", ctx.UserID, email).
|
||||
Delete(&models.UserTrustedSender{}); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("remove trusted sender failed")
|
||||
response.NewErrorResponse(response.ServerError, "取消信任发件人失败", "").FPrint(w)
|
||||
response.NewErrorResponse(response.ServerError, "添加信任发件人失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.NewSuccessResponse("ok").FPrint(w)
|
||||
}
|
||||
|
||||
// SenderFiltersList 查询当前用户的发件人黑白名单列表(支持分页、搜索、类型筛选与统计)
|
||||
func SenderFiltersList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
filterType := strings.ToLower(strings.TrimSpace(query.Get("filter_type")))
|
||||
keyword := strings.ToLower(strings.TrimSpace(query.Get("keyword")))
|
||||
page, _ := strconv.Atoi(query.Get("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, _ := strconv.Atoi(query.Get("page_size"))
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
session := db.Instance.Table(&models.UserTrustedSender{}).Where("user_id = ?", ctx.UserID)
|
||||
if filterType == models.FilterTypeWhitelist || filterType == models.FilterTypeBlacklist {
|
||||
session = session.And("filter_type = ?", filterType)
|
||||
}
|
||||
if keyword != "" {
|
||||
session = session.And("LOWER(email) LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
var list []models.UserTrustedSender
|
||||
total, err := session.OrderBy("created_at DESC").Limit(pageSize, offset).FindAndCount(&list)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("query sender filters failed")
|
||||
response.NewErrorResponse(response.ServerError, "查询过滤名单失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
whitelistCount, _ := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND filter_type = ?", ctx.UserID, models.FilterTypeWhitelist).
|
||||
Count(&models.UserTrustedSender{})
|
||||
blacklistCount, _ := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND filter_type = ?", ctx.UserID, models.FilterTypeBlacklist).
|
||||
Count(&models.UserTrustedSender{})
|
||||
|
||||
response.NewSuccessResponse(map[string]any{
|
||||
"list": list,
|
||||
"total": total,
|
||||
"whitelist_count": whitelistCount,
|
||||
"blacklist_count": blacklistCount,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}).FPrint(w)
|
||||
}
|
||||
|
||||
// AddSenderFilter 添加或更新当前用户的白名单/黑名单
|
||||
func AddSenderFilter(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
var reqData senderFilterRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&reqData); err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(reqData.Email))
|
||||
if email == "" || !address.IsValidEmailAddress(email) {
|
||||
response.NewErrorResponse(response.ParamsError, "邮箱格式不正确", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
filterType := strings.ToLower(strings.TrimSpace(reqData.FilterType))
|
||||
if filterType == "" {
|
||||
filterType = models.FilterTypeWhitelist
|
||||
}
|
||||
if filterType != models.FilterTypeWhitelist && filterType != models.FilterTypeBlacklist {
|
||||
response.NewErrorResponse(response.ParamsError, "名单类别必须为白名单或黑名单", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var existing models.UserTrustedSender
|
||||
has, err := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND LOWER(email) = ?", ctx.UserID, email).
|
||||
Get(&existing)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("check sender filter exists failed")
|
||||
response.NewErrorResponse(response.ServerError, "操作失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if has {
|
||||
existing.FilterType = filterType
|
||||
existing.UpdatedAt = &now
|
||||
if _, err := db.Instance.ID(existing.ID).Cols("filter_type", "updated_at").Update(&existing); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("update sender filter failed")
|
||||
response.NewErrorResponse(response.ServerError, "更新过滤名单失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
item := models.UserTrustedSender{
|
||||
UserID: ctx.UserID,
|
||||
Email: email,
|
||||
FilterType: filterType,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: &now,
|
||||
}
|
||||
if _, err := db.Instance.Insert(&item); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("insert sender filter failed")
|
||||
response.NewErrorResponse(response.ServerError, "添加过滤名单失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.NewSuccessResponse("ok").FPrint(w)
|
||||
}
|
||||
|
||||
// DeleteSenderFilter 删除发件人黑白名单条目(支持ID或邮箱地址删除)
|
||||
func DeleteSenderFilter(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
var reqData senderFilterRequest
|
||||
if req.Body != nil {
|
||||
_ = json.NewDecoder(req.Body).Decode(&reqData)
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(reqData.Email))
|
||||
if reqData.ID <= 0 && email == "" {
|
||||
if idVal, err := strconv.Atoi(req.URL.Query().Get("id")); err == nil && idVal > 0 {
|
||||
reqData.ID = idVal
|
||||
}
|
||||
if qEmail := strings.ToLower(strings.TrimSpace(req.URL.Query().Get("email"))); qEmail != "" {
|
||||
email = qEmail
|
||||
}
|
||||
}
|
||||
|
||||
session := db.Instance.Table(&models.UserTrustedSender{}).Where("user_id = ?", ctx.UserID)
|
||||
if reqData.ID > 0 {
|
||||
session = session.And("id = ?", reqData.ID)
|
||||
} else if email != "" {
|
||||
session = session.And("LOWER(email) = ?", email)
|
||||
} else {
|
||||
response.NewErrorResponse(response.ParamsError, "请指定要删除的记录ID或发件人邮箱", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := session.Delete(&models.UserTrustedSender{}); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("delete sender filter failed")
|
||||
response.NewErrorResponse(response.ServerError, "操作失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
response.NewSuccessResponse("ok").FPrint(w)
|
||||
}
|
||||
|
||||
// ToggleSenderFilter 快速切换发件人的白名单/黑名单状态
|
||||
func ToggleSenderFilter(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if ctx == nil || ctx.UserID <= 0 {
|
||||
response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
var reqData toggleSenderFilterRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&reqData); err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(reqData.Email))
|
||||
if email == "" || !address.IsValidEmailAddress(email) {
|
||||
response.NewErrorResponse(response.ParamsError, "邮箱格式不正确", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var existing models.UserTrustedSender
|
||||
has, err := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND LOWER(email) = ?", ctx.UserID, email).
|
||||
Get(&existing)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("query sender filter for toggle failed")
|
||||
response.NewErrorResponse(response.ServerError, "操作失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
targetType := strings.ToLower(strings.TrimSpace(reqData.TargetType))
|
||||
if !has {
|
||||
if targetType != models.FilterTypeWhitelist && targetType != models.FilterTypeBlacklist {
|
||||
targetType = models.FilterTypeBlacklist
|
||||
}
|
||||
item := models.UserTrustedSender{
|
||||
UserID: ctx.UserID,
|
||||
Email: email,
|
||||
FilterType: targetType,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: &now,
|
||||
}
|
||||
if _, err := db.Instance.Insert(&item); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("insert toggled sender filter failed")
|
||||
response.NewErrorResponse(response.ServerError, "操作失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if targetType == "" {
|
||||
if existing.FilterType == models.FilterTypeBlacklist {
|
||||
targetType = models.FilterTypeWhitelist
|
||||
} else {
|
||||
targetType = models.FilterTypeBlacklist
|
||||
}
|
||||
}
|
||||
existing.FilterType = targetType
|
||||
existing.UpdatedAt = &now
|
||||
if _, err := db.Instance.ID(existing.ID).Cols("filter_type", "updated_at").Update(&existing); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("update toggled sender filter failed")
|
||||
response.NewErrorResponse(response.ServerError, "操作失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,3 +133,107 @@ func TestTrustSenderToggle(t *testing.T) {
|
||||
t.Fatalf("expected trusted sender to be removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSenderFiltersCRUDAndTenantIsolation(t *testing.T) {
|
||||
setupContactsTestDB(t)
|
||||
ctxUser1 := &context.Context{UserID: 1}
|
||||
ctxUser2 := &context.Context{UserID: 2}
|
||||
|
||||
// 1. User 1 添加白名单
|
||||
addBody, _ := json.Marshal(senderFilterRequest{Email: "partner@example.com", FilterType: models.FilterTypeWhitelist})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/email/sender-filters", bytes.NewReader(addBody))
|
||||
w := httptest.NewRecorder()
|
||||
AddSenderFilter(ctxUser1, w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// 2. User 1 添加黑名单
|
||||
addBody2, _ := json.Marshal(senderFilterRequest{Email: "spammer@badsite.com", FilterType: models.FilterTypeBlacklist})
|
||||
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/email/sender-filters", bytes.NewReader(addBody2))
|
||||
w2 := httptest.NewRecorder()
|
||||
AddSenderFilter(ctxUser1, w2, req2)
|
||||
if w2.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w2.Code)
|
||||
}
|
||||
|
||||
// 3. User 2 添加名单(用于多租户隔离验证)
|
||||
addBody3, _ := json.Marshal(senderFilterRequest{Email: "secret@user2.com", FilterType: models.FilterTypeWhitelist})
|
||||
req3 := httptest.NewRequest(http.MethodPost, "/api/v1/email/sender-filters", bytes.NewReader(addBody3))
|
||||
w3 := httptest.NewRecorder()
|
||||
AddSenderFilter(ctxUser2, w3, req3)
|
||||
if w3.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w3.Code)
|
||||
}
|
||||
|
||||
// 4. User 1 查询列表(应有 2 条:1 白 1 黑,统计正确)
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/email/sender-filters", nil)
|
||||
listW := httptest.NewRecorder()
|
||||
SenderFiltersList(ctxUser1, listW, listReq)
|
||||
if listW.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", listW.Code)
|
||||
}
|
||||
|
||||
var listResp struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
List []models.UserTrustedSender `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
WhitelistCount int64 `json:"whitelist_count"`
|
||||
BlacklistCount int64 `json:"blacklist_count"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(listW.Body.Bytes(), &listResp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if listResp.Data.Total != 2 || listResp.Data.WhitelistCount != 1 || listResp.Data.BlacklistCount != 1 {
|
||||
t.Fatalf("unexpected counts: total=%d, white=%d, black=%d",
|
||||
listResp.Data.Total, listResp.Data.WhitelistCount, listResp.Data.BlacklistCount)
|
||||
}
|
||||
for _, item := range listResp.Data.List {
|
||||
if item.Email == "secret@user2.com" {
|
||||
t.Fatalf("tenant isolation violation: user1 saw user2 filter")
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 快速切换(Toggle)白名单 -> 黑名单
|
||||
toggleBody, _ := json.Marshal(toggleSenderFilterRequest{Email: "partner@example.com"})
|
||||
toggleReq := httptest.NewRequest(http.MethodPost, "/api/v1/email/sender-filters/toggle", bytes.NewReader(toggleBody))
|
||||
toggleW := httptest.NewRecorder()
|
||||
ToggleSenderFilter(ctxUser1, toggleW, toggleReq)
|
||||
if toggleW.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", toggleW.Code)
|
||||
}
|
||||
|
||||
var updated models.UserTrustedSender
|
||||
has, err := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND LOWER(email) = ?", 1, "partner@example.com").
|
||||
Get(&updated)
|
||||
if err != nil || !has || updated.FilterType != models.FilterTypeBlacklist {
|
||||
t.Fatalf("expected partner@example.com to be toggled to blacklist, got filter_type=%s", updated.FilterType)
|
||||
}
|
||||
|
||||
// 6. 删除黑名单条目
|
||||
delBody, _ := json.Marshal(senderFilterRequest{Email: "spammer@badsite.com"})
|
||||
delReq := httptest.NewRequest(http.MethodDelete, "/api/v1/email/sender-filters", bytes.NewReader(delBody))
|
||||
delW := httptest.NewRecorder()
|
||||
DeleteSenderFilter(ctxUser1, delW, delReq)
|
||||
if delW.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", delW.Code)
|
||||
}
|
||||
|
||||
hasSpammer, _ := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND LOWER(email) = ?", 1, "spammer@badsite.com").
|
||||
Exist(&models.UserTrustedSender{})
|
||||
if hasSpammer {
|
||||
t.Fatalf("expected spammer to be deleted")
|
||||
}
|
||||
|
||||
// 7. 再次确认 User 2 数据完整未受影响
|
||||
hasU2, _ := db.Instance.Table(&models.UserTrustedSender{}).
|
||||
Where("user_id = ? AND LOWER(email) = ?", 2, "secret@user2.com").
|
||||
Exist(&models.UserTrustedSender{})
|
||||
if !hasU2 {
|
||||
t.Fatalf("user 2 data was corrupted")
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,8 @@ type emilItem struct {
|
||||
Sender User `json:"sender"`
|
||||
To []User `json:"to"`
|
||||
Dangerous bool `json:"dangerous"`
|
||||
IsTrusted bool `json:"is_trusted"`
|
||||
IsBlacklisted bool `json:"is_blacklisted"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
@ -81,11 +83,17 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
response.NewErrorResponse(response.ServerError, "读取邮件列表失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
trustedMap := make(map[string]bool)
|
||||
var trustedSenders []models.UserTrustedSender
|
||||
if err := db.Instance.Table(&models.UserTrustedSender{}).Where("user_id = ?", ctx.UserID).Find(&trustedSenders); err == nil {
|
||||
for _, item := range trustedSenders {
|
||||
trustedMap[strings.ToLower(strings.TrimSpace(item.Email))] = true
|
||||
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 {
|
||||
for _, item := range senderFilters {
|
||||
addr := strings.ToLower(strings.TrimSpace(item.Email))
|
||||
if item.FilterType == models.FilterTypeBlacklist {
|
||||
blacklistMap[addr] = true
|
||||
} else {
|
||||
whitelistMap[addr] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,8 +117,13 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
authentication := parsemail.NewEmailAuthentication(email.SPFCheck == 1, email.DKIMCheck == 1)
|
||||
senderAddr := strings.ToLower(strings.TrimSpace(sender.EmailAddress))
|
||||
isTrusted := whitelistMap[senderAddr]
|
||||
isBlacklisted := blacklistMap[senderAddr]
|
||||
dangerous := authentication.Dangerous
|
||||
if trustedMap[strings.ToLower(strings.TrimSpace(sender.EmailAddress))] {
|
||||
if isBlacklisted {
|
||||
dangerous = true
|
||||
} else if isTrusted {
|
||||
dangerous = false
|
||||
}
|
||||
items = append(items, &emilItem{
|
||||
@ -122,6 +135,8 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
Sender: sender,
|
||||
To: recipients,
|
||||
Dangerous: dangerous,
|
||||
IsTrusted: isTrusted,
|
||||
IsBlacklisted: isBlacklisted,
|
||||
Error: email.Error.String,
|
||||
})
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ func DefaultMigrations() []Migration {
|
||||
DefineCustom(18, "expand_operation_log_actor_role", "expand_operation_log_actor_role_v1", applyOperationLogActorRoleSchema),
|
||||
DefineForDialects(19, "create_user_contacts_and_trusted_senders", userContactStatements()),
|
||||
DefineCustom(20, "expand_protocol_credential_ciphertext", "expand_protocol_credential_ciphertext_v1", applyProtocolCredentialCiphertextSchema),
|
||||
DefineCustom(21, "expand_trusted_sender_blacklist", "expand_trusted_sender_blacklist_v1", applyTrustedSenderBlacklistSchema),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -62,6 +62,15 @@ func TestDefaultMigrationsCreateCompleteSQLiteSchema(t *testing.T) {
|
||||
if !exists {
|
||||
t.Fatal("column operation_log.actor_role was not created")
|
||||
}
|
||||
for _, column := range []string{"filter_type", "updated_at"} {
|
||||
exists, err := columnExists(context.Background(), db, DialectSQLite, "user_trusted_sender", column)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("column user_trusted_sender.%s was not created", column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sqliteIndexExists(t *testing.T, db *sql.DB, index string) bool {
|
||||
@ -285,6 +294,7 @@ func TestDefaultMigrationChecksumsAreImmutable(t *testing.T) {
|
||||
18: "17622df3406657f199d78a3a1aa1f3a8693baf85aa7707910cc6d6cb44c13ee4",
|
||||
19: "f24b94ca9e4c25b7973f6c15fcaddccc7f02bcf7886c0a4995d2f7b79f4a3a16",
|
||||
20: "e0bc01f4c2df742bc23c519c2a399cc1c3f341565dbed014509a8c63d68bf725",
|
||||
21: "ac1a0b3be90128c0e0512d2cec858f8cd7e70abc5ff4734d7113b9674f08dbb0",
|
||||
}
|
||||
migrations := DefaultMigrations()
|
||||
if len(migrations) != len(expected) {
|
||||
|
||||
33
backend/db/migration/trusted_sender_blacklist.go
Normal file
33
backend/db/migration/trusted_sender_blacklist.go
Normal file
@ -0,0 +1,33 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func applyTrustedSenderBlacklistSchema(ctx context.Context, exec executor, dialect Dialect) error {
|
||||
filterTypeDefinitions := map[Dialect]string{
|
||||
DialectSQLite: "TEXT NOT NULL DEFAULT 'whitelist'",
|
||||
DialectMySQL: "VARCHAR(20) NOT NULL DEFAULT 'whitelist'",
|
||||
DialectPostgres: "VARCHAR(20) NOT NULL DEFAULT 'whitelist'",
|
||||
}
|
||||
updatedAtDefinitions := map[Dialect]string{
|
||||
DialectSQLite: "DATETIME NULL",
|
||||
DialectMySQL: "DATETIME(6) NULL",
|
||||
DialectPostgres: "TIMESTAMPTZ NULL",
|
||||
}
|
||||
|
||||
ftDef, ok := filterTypeDefinitions[dialect]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported migration dialect %q for filter_type", dialect)
|
||||
}
|
||||
if err := ensureColumn(ctx, exec, dialect, "user_trusted_sender", "filter_type", ftDef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upDef, ok := updatedAtDefinitions[dialect]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported migration dialect %q for updated_at", dialect)
|
||||
}
|
||||
return ensureColumn(ctx, exec, dialect, "user_trusted_sender", "updated_at", upDef)
|
||||
}
|
||||
@ -62,6 +62,9 @@ func registerV1Routes(mux *http.ServeMux) {
|
||||
registerV1MethodRoute(mux, "/api/v1/email/contacts", "/api/email/contacts", http.MethodGet, 0, email.ContactsList)
|
||||
registerV1MethodRoute(mux, "/api/v1/email/contacts/delete", "/api/email/contacts/delete", http.MethodPost, 16<<10, email.DeleteContact)
|
||||
registerV1MethodRoute(mux, "/api/v1/email/trust-sender", "/api/email/trust-sender", http.MethodPost, 16<<10, email.TrustSender)
|
||||
registerV1SenderFiltersRoute(mux)
|
||||
registerV1MethodRoute(mux, "/api/v1/email/sender-filters/delete", "/api/email/sender-filters/delete", http.MethodPost, 16<<10, email.DeleteSenderFilter)
|
||||
registerV1MethodRoute(mux, "/api/v1/email/sender-filters/toggle", "/api/email/sender-filters/toggle", http.MethodPost, 16<<10, email.ToggleSenderFilter)
|
||||
registerV1MethodRoute(mux, "/api/v1/settings/modify_password", "/api/settings/modify_password", http.MethodPost, 64<<10, controllers.ModifyPassword)
|
||||
registerV1MethodRoute(mux, "/api/v1/rule/get", "/api/rule/get", http.MethodPost, 4<<10, controllers.GetRule)
|
||||
registerV1MethodRoute(mux, "/api/v1/rule/add", "/api/rule/add", http.MethodPost, 64<<10, controllers.UpsertRule)
|
||||
@ -89,6 +92,25 @@ func registerV1MethodRoute(mux *http.ServeMux, path string, legacyPath string, m
|
||||
mux.HandleFunc(path, onlyMethod(method, wrapped))
|
||||
}
|
||||
|
||||
func registerV1SenderFiltersRoute(mux *http.ServeMux) {
|
||||
get := contextIterceptor(rewriteV1Path("/api/email/sender-filters", email.SenderFiltersList))
|
||||
post := limitBody(16<<10, contextIterceptor(rewriteV1Path("/api/email/sender-filters", email.AddSenderFilter)))
|
||||
del := limitBody(16<<10, contextIterceptor(rewriteV1Path("/api/email/sender-filters", email.DeleteSenderFilter)))
|
||||
mux.HandleFunc("/api/v1/email/sender-filters", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
get.ServeHTTP(w, r)
|
||||
case http.MethodPost:
|
||||
post.ServeHTTP(w, r)
|
||||
case http.MethodDelete:
|
||||
del.ServeHTTP(w, r)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST, DELETE")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func registerV1CredentialsQuestionRoute(mux *http.ServeMux) {
|
||||
get := contextIterceptor(rewriteV1Path("/api/security/credentials/question", controllers.GetSecurityQuestion))
|
||||
post := limitBody(16<<10, contextIterceptor(rewriteV1Path("/api/security/credentials/question", controllers.SetSecurityQuestion)))
|
||||
|
||||
@ -92,6 +92,21 @@ func router(mux *http.ServeMux) error {
|
||||
mux.HandleFunc("/api/email/contacts", onlyMethod(http.MethodGet, contextIterceptor(email.ContactsList)))
|
||||
mux.HandleFunc("/api/email/contacts/delete", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(email.DeleteContact))))
|
||||
mux.HandleFunc("/api/email/trust-sender", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(email.TrustSender))))
|
||||
mux.HandleFunc("/api/email/sender-filters", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
contextIterceptor(email.SenderFiltersList)(w, r)
|
||||
case http.MethodPost:
|
||||
limitBody(16<<10, contextIterceptor(email.AddSenderFilter))(w, r)
|
||||
case http.MethodDelete:
|
||||
limitBody(16<<10, contextIterceptor(email.DeleteSenderFilter))(w, r)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST, DELETE")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/email/sender-filters/delete", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(email.DeleteSenderFilter))))
|
||||
mux.HandleFunc("/api/email/sender-filters/toggle", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(email.ToggleSenderFilter))))
|
||||
mux.HandleFunc("/api/settings/modify_password", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.ModifyPassword))))
|
||||
mux.HandleFunc("/api/rule/get", onlyMethod(http.MethodPost, limitBody(4<<10, contextIterceptor(controllers.GetRule))))
|
||||
mux.HandleFunc("/api/rule/add", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.UpsertRule))))
|
||||
|
||||
@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// UserContact 保存单个用户的历史写信收件人,用于写信页自动补全与联系人管理
|
||||
// UserContact 保存单个用户的厇史写信收仴人,用于写信页自动补全与遗系人管理
|
||||
type UserContact struct {
|
||||
ID int `xorm:"id bigint unsigned not null pk autoincr" json:"id"`
|
||||
UserID int `xorm:"user_id bigint not null" json:"-"`
|
||||
@ -13,12 +13,19 @@ type UserContact struct {
|
||||
|
||||
func (UserContact) TableName() string { return "user_contact" }
|
||||
|
||||
// UserTrustedSender 保存单个用户手动标记信任的发件人白名单,免除安全警告
|
||||
const (
|
||||
FilterTypeWhitelist = "whitelist"
|
||||
FilterTypeBlacklist = "blacklist"
|
||||
)
|
||||
|
||||
// UserTrustedSender 保存单个用户标记的白名单(信任)与黑名单发件人,控制收信安全警告与拒信/风险标议
|
||||
type UserTrustedSender struct {
|
||||
ID int `xorm:"id bigint unsigned not null pk autoincr" json:"id"`
|
||||
UserID int `xorm:"user_id bigint not null" json:"-"`
|
||||
Email string `xorm:"email varchar(255) notnull" json:"email"`
|
||||
FilterType string `xorm:"filter_type varchar(20) notnull default('whitelist')" json:"filter_type"`
|
||||
CreatedAt time.Time `xorm:"created_at datetime created notnull" json:"created_at"`
|
||||
UpdatedAt *time.Time `xorm:"updated_at datetime updated null" json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func (UserTrustedSender) TableName() string { return "user_trusted_sender" }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user