From 64749a578b67e73dd99012923b98d947e8a7d8dd Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 30 Aug 2026 23:45:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=B9=B6=E5=8F=91=E7=AB=9E=E4=BA=89=E4=B8=8E?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E5=AD=97=E6=AE=B5=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config/config.go | 13 ++++++++++++- server/config/runtime_access.go | 2 +- .../listen/http_server/api_v1_compat_test.go | 4 ++-- server/listen/http_server/https_server_test.go | 8 ++++---- .../http_server/password_change_route_test.go | 8 ++++---- .../http_server/session_validation_test.go | 4 ++-- .../listen/http_server/setup_security_test.go | 18 +++++++++--------- server/models/user_email.go | 2 +- server/services/auth/auth_test.go | 3 ++- server/services/auth/identifier_test.go | 6 ++++-- server/services/setup/finish.go | 4 ++-- 11 files changed, 43 insertions(+), 29 deletions(-) diff --git a/server/config/config.go b/server/config/config.go index 6389eee6..13edf0d8 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -12,10 +12,12 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "time" ) var IsInit bool +var legacyInitState atomic.Bool var DevelopmentMode bool type Config struct { @@ -197,7 +199,7 @@ func Init() error { Instance.Domains = []string{Instance.Domain} } - IsInit = Instance.IsSetupReady() + SetInitState(Instance.IsSetupReady()) PublishConfigSnapshot(Instance) // 设置日志格式为json格式 @@ -238,6 +240,15 @@ func (c *Config) IsSetupReady() bool { return c != nil && c.SetupState == SetupStateReady } +func SetInitState(value bool) { + legacyInitState.Store(value) + IsInit = value +} + +func legacyInitReady() bool { + return legacyInitState.Load() +} + func ReadPrivateKey() (*ecdsa.PrivateKey, bool, error) { privateKey, created, err := loadOrCreateACMEPrivateKey(filepath.Join(ROOT_PATH, "config", "ssl"), "account_private.pem") if err != nil { diff --git a/server/config/runtime_access.go b/server/config/runtime_access.go index 937c8cef..933864a5 100644 --- a/server/config/runtime_access.go +++ b/server/config/runtime_access.go @@ -16,7 +16,7 @@ func IsSetupReady() bool { if value != nil && value.SetupState.Valid() { return value.SetupState == SetupStateReady } - return IsInit + return legacyInitReady() } // PublishConfigSnapshot 发布一份深拷贝,供运行时只读访问。 diff --git a/server/listen/http_server/api_v1_compat_test.go b/server/listen/http_server/api_v1_compat_test.go index 88928b91..3b4bf18c 100644 --- a/server/listen/http_server/api_v1_compat_test.go +++ b/server/listen/http_server/api_v1_compat_test.go @@ -99,8 +99,8 @@ func TestV1RouteDoesNotReceiveLegacyDeprecationHeaders(t *testing.T) { func TestV1LoginUsesSameRequestOriginGuard(t *testing.T) { oldInit := config.IsInit - t.Cleanup(func() { config.IsInit = oldInit }) - config.IsInit = false + t.Cleanup(func() { config.SetInitState(oldInit) }) + config.SetInitState(false) handler := contextIterceptor(controllers.Login) request := httptest.NewRequest(http.MethodPost, "http://mail.example.com/api/v1/login", nil) diff --git a/server/listen/http_server/https_server_test.go b/server/listen/http_server/https_server_test.go index 39d524a6..35c1eefd 100644 --- a/server/listen/http_server/https_server_test.go +++ b/server/listen/http_server/https_server_test.go @@ -47,10 +47,10 @@ func TestCSRFRejectsCrossOriginCookieRequest(t *testing.T) { oldInit := config.IsInit oldSession := session.Instance defer func() { - config.IsInit = oldInit + config.SetInitState(oldInit) session.Instance = oldSession }() - config.IsInit = false + config.SetInitState(false) session.Instance = scs.New() session.Instance.Cookie.Name = "smail_session" @@ -81,10 +81,10 @@ func TestCSRFAllowsSameOriginCookieRequest(t *testing.T) { oldInit := config.IsInit oldSession := session.Instance defer func() { - config.IsInit = oldInit + config.SetInitState(oldInit) session.Instance = oldSession }() - config.IsInit = false + config.SetInitState(false) session.Instance = scs.New() session.Instance.Cookie.Name = "smail_session" diff --git a/server/listen/http_server/password_change_route_test.go b/server/listen/http_server/password_change_route_test.go index f78a83c4..d6cec323 100644 --- a/server/listen/http_server/password_change_route_test.go +++ b/server/listen/http_server/password_change_route_test.go @@ -25,12 +25,12 @@ func TestModifyPasswordRouteRejectsUnauthenticatedRequest(t *testing.T) { oldSession := session.Instance t.Cleanup(func() { config.Instance = oldConfig - config.IsInit = oldInit + config.SetInitState(oldInit) session.Instance = oldSession }) config.Instance = &config.Config{} - config.IsInit = true + config.SetInitState(true) session.Instance = scs.New() mux := http.NewServeMux() @@ -66,7 +66,7 @@ func TestSuperAdminCannotChangeOwnPasswordAfterLogin(t *testing.T) { _ = db.Instance.Close() } config.Instance = oldConfig - config.IsInit = oldInit + config.SetInitState(oldInit) db.Instance = oldDB session.Instance = oldSession }) @@ -75,7 +75,7 @@ func TestSuperAdminCannotChangeOwnPasswordAfterLogin(t *testing.T) { DbType: config.DBTypeSQLite, DbDSN: filepath.Join(testRoot, "super-admin-password.db"), } - config.IsInit = true + config.SetInitState(true) if err := db.Init(""); err != nil { t.Fatal(err) } diff --git a/server/listen/http_server/session_validation_test.go b/server/listen/http_server/session_validation_test.go index bc180ce8..2011432c 100644 --- a/server/listen/http_server/session_validation_test.go +++ b/server/listen/http_server/session_validation_test.go @@ -29,13 +29,13 @@ func TestContextInterceptorValidatesCurrentUserState(t *testing.T) { _ = db.Instance.Close() } config.Instance = oldConfig - config.IsInit = oldInit + config.SetInitState(oldInit) db.Instance = oldDB session.Instance = oldSession }) config.Instance = &config.Config{DbType: config.DBTypeSQLite, DbDSN: filepath.Join(testRoot, "session-validation.db")} - config.IsInit = true + config.SetInitState(true) if err := db.Init(""); err != nil { t.Fatal(err) } diff --git a/server/listen/http_server/setup_security_test.go b/server/listen/http_server/setup_security_test.go index deb8a128..3999f768 100644 --- a/server/listen/http_server/setup_security_test.go +++ b/server/listen/http_server/setup_security_test.go @@ -21,9 +21,9 @@ func withSetupSecurity(t *testing.T, token string) *setupSecurity { if err := os.Unsetenv("SMAIL_SETUP_ALLOWED_CIDRS"); err != nil { t.Fatal(err) } - config.IsInit = false + config.SetInitState(false) t.Cleanup(func() { - config.IsInit = oldInit + config.SetInitState(oldInit) _ = os.Setenv("SMAIL_SETUP_TOKEN", oldToken) _ = os.Setenv("SMAIL_SETUP_ALLOWED_CIDRS", oldCIDRs) }) @@ -65,7 +65,7 @@ func TestSetupSecurityAcceptsTokenOnce(t *testing.T) { calls := 0 handler := security.authorize(func(w http.ResponseWriter, r *http.Request) { calls++ - config.IsInit = true + config.SetInitState(true) w.WriteHeader(http.StatusNoContent) }) @@ -77,7 +77,7 @@ func TestSetupSecurityAcceptsTokenOnce(t *testing.T) { t.Fatalf("first request failed: code=%d calls=%d", recorder.Code, calls) } - config.IsInit = false + config.SetInitState(false) request = setupRequest(http.MethodPost) request.Header.Set("X-Setup-Token", token) recorder = httptest.NewRecorder() @@ -100,7 +100,7 @@ func TestSetupSecuritySerializesConcurrentTokenRequests(t *testing.T) { callsMu.Unlock() close(started) <-continueRequest - config.IsInit = true + config.SetInitState(true) w.WriteHeader(http.StatusNoContent) }) @@ -243,7 +243,7 @@ func TestSetupAPIRoutesBecomeInvalidAfterSetupCompletes(t *testing.T) { calls := 0 registerSetupAPIRoutes(mux, security, setupOnlyPost(func(w http.ResponseWriter, r *http.Request) { calls++ - config.IsInit = true + config.SetInitState(true) w.WriteHeader(http.StatusNoContent) })) @@ -256,7 +256,7 @@ func TestSetupAPIRoutesBecomeInvalidAfterSetupCompletes(t *testing.T) { t.Fatalf("first setup returned code=%d calls=%d", recorder.Code, calls) } - config.IsInit = false + config.SetInitState(false) for _, endpoint := range []string{"/api/setup", "/api/v1/setup"} { request = httptest.NewRequest(http.MethodPost, endpoint, nil) request.RemoteAddr = "127.0.0.1:12345" @@ -280,9 +280,9 @@ func TestSetupSecurityBootstrapCookieForAllowedRemote(t *testing.T) { if err := os.Setenv("SMAIL_SETUP_ALLOWED_CIDRS", "192.168.2.0/24"); err != nil { t.Fatal(err) } - config.IsInit = false + config.SetInitState(false) t.Cleanup(func() { - config.IsInit = oldInit + config.SetInitState(oldInit) _ = os.Setenv("SMAIL_SETUP_TOKEN", oldToken) _ = os.Setenv("SMAIL_SETUP_ALLOWED_CIDRS", oldCIDRs) }) diff --git a/server/models/user_email.go b/server/models/user_email.go index 2d0fe197..a500ad64 100644 --- a/server/models/user_email.go +++ b/server/models/user_email.go @@ -7,7 +7,7 @@ type UserEmail struct { UserID int `xorm:"user_id int not null index('idx_eid') index('idx_user_email_user_primary')"` EmailID int `xorm:"email_id not null index('idx_eid') index"` Address string `xorm:"address varchar(254) null" json:"address,omitempty"` - AddressCanonical string `xorm:"address_canonical varchar(254) null unique" json:"-"` + AddressCanonical *string `xorm:"address_canonical varchar(254) null unique" json:"-"` IsPrimary int8 `xorm:"is_primary tinyint(1) notnull default(0)" json:"is_primary"` VerifiedAt *time.Time `xorm:"verified_at datetime null" json:"verified_at,omitempty"` CreatedAt time.Time `xorm:"created_at datetime created" json:"created_at"` diff --git a/server/services/auth/auth_test.go b/server/services/auth/auth_test.go index b83e380d..d2a2690c 100644 --- a/server/services/auth/auth_test.go +++ b/server/services/auth/auth_test.go @@ -135,10 +135,11 @@ func TestProtocolAuthenticationAcceptsConfiguredFullEmailIdentifier(t *testing.T t.Fatal(err) } verifiedAt := time.Now().UTC() + canonicalAddress := "alice@example.com" if _, err := db.Instance.Insert(&models.UserEmail{ UserID: user.ID, Address: "alice@example.com", - AddressCanonical: "alice@example.com", + AddressCanonical: &canonicalAddress, VerifiedAt: &verifiedAt, }); err != nil { t.Fatal(err) diff --git a/server/services/auth/identifier_test.go b/server/services/auth/identifier_test.go index 9693ba7e..3551d190 100644 --- a/server/services/auth/identifier_test.go +++ b/server/services/auth/identifier_test.go @@ -37,18 +37,20 @@ func TestResolveIdentifierUsesVerifiedFullEmailBinding(t *testing.T) { t.Fatal(err) } verifiedAt := time.Now().UTC() + canonicalAddress := "alice@example.com" if _, err := db.Instance.Insert(&models.UserEmail{ UserID: user.ID, Address: "Alice@Example.com", - AddressCanonical: "alice@example.com", + AddressCanonical: &canonicalAddress, VerifiedAt: &verifiedAt, }); err != nil { t.Fatal(err) } + pendingAddress := "pending@example.com" if _, err := db.Instance.Insert(&models.UserEmail{ UserID: user.ID, Address: "pending@example.com", - AddressCanonical: "pending@example.com", + AddressCanonical: &pendingAddress, }); err != nil { t.Fatal(err) } diff --git a/server/services/setup/finish.go b/server/services/setup/finish.go index 924e454c..48b1ad69 100644 --- a/server/services/setup/finish.go +++ b/server/services/setup/finish.go @@ -18,7 +18,7 @@ func Finish() error { return errors.Wrap(err) } if cfg.IsSetupReady() { - config.IsInit = true + config.SetInitState(true) return nil } cfg.SetupState = config.SetupStateReady @@ -28,7 +28,7 @@ func Finish() error { if err != nil { return errors.Wrap(err) } - config.IsInit = true + config.SetInitState(true) // 初始化完成 select { case signal.InitChan <- true: