From f6566c7aa9853af54a24517607818c3003356dd7 Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 6 Sep 2026 06:06:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=99=84=E4=BB=B6=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=92=8C=E5=AE=89=E5=85=A8=E9=82=AE=E7=AE=B1=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/attachments.go | 6 ++--- backend/controllers/security.go | 30 ++++++++++++++++++++++ backend/listen/http_server/https_server.go | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/backend/controllers/attachments.go b/backend/controllers/attachments.go index 598f027..8060e74 100644 --- a/backend/controllers/attachments.go +++ b/backend/controllers/attachments.go @@ -36,12 +36,12 @@ func GetAttachments(ctx *context.Context, w http.ResponseWriter, req *http.Reque func Download(ctx *context.Context, w http.ResponseWriter, req *http.Request) { urlInfos := strings.Split(strings.Trim(req.URL.Path, "/"), "/") - if len(urlInfos) != 5 { + if len(urlInfos) != 4 || urlInfos[0] != "attachments" || urlInfos[1] != "download" { response.NewErrorResponse(response.ParamsError, "", "").FPrint(w) return } - emailId := cast.ToInt(urlInfos[3]) - index := cast.ToInt(urlInfos[4]) + emailId := cast.ToInt(urlInfos[2]) + index := cast.ToInt(urlInfos[3]) fileName, content, err := attachments.GetAttachmentsByIndex(ctx, emailId, index) if err != nil { diff --git a/backend/controllers/security.go b/backend/controllers/security.go index 03df7c7..6ad579a 100644 --- a/backend/controllers/security.go +++ b/backend/controllers/security.go @@ -3,6 +3,7 @@ package controllers import ( "crypto/rand" "encoding/hex" + "errors" "net" "net/http" "net/url" @@ -12,6 +13,7 @@ import ( "git.pcux.cn/xuans/Smail/config" "git.pcux.cn/xuans/Smail/db" "git.pcux.cn/xuans/Smail/dto/response" + "git.pcux.cn/xuans/Smail/models" "git.pcux.cn/xuans/Smail/services/access" "git.pcux.cn/xuans/Smail/services/credentials" "git.pcux.cn/xuans/Smail/services/externalemail" @@ -633,11 +635,31 @@ func BindExternalEmail(ctx *context.Context, w http.ResponseWriter, req *http.Re writeSecurityError(w, response.NeedLogin, "请先登录") return } + if !ctx.RecentAuth { + writeSecurityError(w, response.NoAccessPrivileges, "此操作需要最近十分钟内完成登录,请重新登录后再试") + return + } var input externalEmailRequest if err := decodeJSONRequest(w, req, &input); err != nil { writeSecurityError(w, response.ParamsError, "请求无效") return } + normalizedEmail, err := externalemail.NormalizeEmail(input.Email) + if err != nil { + writeSecurityError(w, response.ParamsError, "外部邮箱绑定失败") + return + } + owned, err := currentUserPrimaryEmail(ctx.UserID, normalizedEmail) + if err != nil { + log.WithError(err).Error("check current user primary email failed") + writeSecurityError(w, response.ServerError, "外部邮箱绑定失败") + return + } + if owned { + writeSecurityError(w, response.ParamsError, "不能将当前邮箱绑定为外部安全邮箱") + return + } + input.Email = normalizedEmail item, err := securityRuntime.external.Bind(externalemail.BindRequest{UserID: ctx.UserID, Email: input.Email, Type: input.Type}) if err != nil { writeSecurityError(w, response.ParamsError, "外部邮箱绑定失败") @@ -652,6 +674,14 @@ func BindExternalEmail(ctx *context.Context, w http.ResponseWriter, req *http.Re response.NewSuccessResponse(map[string]any{"id": item.ID, "challenge_id": challenge.ID, "status": item.Status, "email": maskEmail(input.Email), "expires_at": challenge.ExpiresAt.UTC()}).FPrint(w) } +func currentUserPrimaryEmail(userID int, email string) (bool, error) { + if db.Instance == nil || userID <= 0 { + return false, errors.New("database is unavailable") + } + var binding models.UserEmail + return db.Instance.Where("user_id = ? AND verified_at IS NOT NULL AND status != ? AND is_primary = 1 AND (address_canonical = ? OR LOWER(address) = ?)", userID, 3, email, email).Limit(1).Get(&binding) +} + func VerifyExternalEmail(ctx *context.Context, w http.ResponseWriter, req *http.Request) { if ctx == nil || ctx.UserID <= 0 || securityRuntime.external == nil || securityRuntime.verification == nil { writeSecurityError(w, response.NeedLogin, "请先登录") diff --git a/backend/listen/http_server/https_server.go b/backend/listen/http_server/https_server.go index e7a338f..8367fc9 100644 --- a/backend/listen/http_server/https_server.go +++ b/backend/listen/http_server/https_server.go @@ -193,7 +193,7 @@ func mustChangePasswordAllowedPath(path string) bool { func publicSecurityPath(path string) bool { switch normalizedAPIPath(path) { - case "/api/ping", "/api/login", "/api/security/login/verify", "/api/security/recovery/begin", "/api/security/recovery/verify", "/api/security/recovery/complete", "/api/security/recovery/webauthn/begin", "/api/security/recovery/webauthn/verify", "/api/security/webauthn/login/begin", "/api/security/webauthn/login/finish": + case "/api/ping", "/api/login", "/api/security/login/verify", "/api/security/login/resend", "/api/security/recovery/begin", "/api/security/recovery/verify", "/api/security/recovery/complete", "/api/security/recovery/webauthn/begin", "/api/security/recovery/webauthn/verify", "/api/security/webauthn/login/begin", "/api/security/webauthn/login/finish": return true default: return false