From dbd0a1783e17b7e04cad0b2cc6cab14548e21415 Mon Sep 17 00:00:00 2001 From: UcnacDx2 <127503808+UcnacDx2@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:20:34 +0000 Subject: [PATCH 1/2] fix(drivers/139): improve mail login credential renewal --- drivers/139/meta.go | 8 +- drivers/139/util.go | 177 +++++++------------------------------ drivers/139/util_test.go | 182 ++++++++++++++++++++++++++------------- 3 files changed, 157 insertions(+), 210 deletions(-) diff --git a/drivers/139/meta.go b/drivers/139/meta.go index 86b587784..8e963d705 100644 --- a/drivers/139/meta.go +++ b/drivers/139/meta.go @@ -7,11 +7,11 @@ import ( type Addition struct { //Account string `json:"account" required:"true"` - Authorization string `json:"authorization" type:"text" help:"Authorization can be used alone. If empty, use mail_cookies alone for fast login, or mail_cookies + username + password for full login fallback."` - Username string `json:"username" help:"Required only when using password login fallback with mail_cookies."` - Password string `json:"password" secret:"true" help:"Required only when using password login fallback with mail_cookies."` + Authorization string `json:"authorization" type:"text" help:"Authorization can be used alone. If empty, use username + password; mail_cookies is optional and will be established/updated automatically. Existing mail_cookies can also be used alone for fast login."` + Username string `json:"username" help:"Use together with password when Authorization is empty. mail_cookies may be left empty on the first login."` + Password string `json:"password" secret:"true" help:"Use together with username when Authorization is empty. mail_cookies may be left empty on the first login."` SmsCode string `json:"sms_code" secret:"true" help:"Fill this only after OpenList reports that a 139 Mail SMS verification code was sent, then save the storage again."` - MailCookies string `json:"mail_cookies" type:"text" help:"Cookies from mail.10086.cn. Used for fast login only when Authorization is empty; otherwise retained as device context for password login fallback."` + MailCookies string `json:"mail_cookies" type:"text" help:"Optional cookies from mail.10086.cn. Leave empty for a first username/password login; cookies created or updated by password/SMS login are persisted and reused as device context. Existing cookies may also be used alone for fast login."` driver.RootID Type string `json:"type" type:"select" options:"personal_new,family,group,personal,share" default:"personal_new"` LinkID string `json:"link_id" type:"text" help:"Multiple shares are separated by commas or new lines. Use link_id#password for password-protected shares."` diff --git a/drivers/139/util.go b/drivers/139/util.go index b5fa9130a..44465db81 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -46,31 +46,10 @@ const ( ) var ( - mailRootURL = "https://mail.10086.cn/" mailPasswordURL = "https://mail.10086.cn/Login/Login.ashx" mailSMSURL = "https://mail.10086.cn/s" ) -var mailLoginCookieExclusions = map[string]struct{}{ - "hecaiyun_stay_time": {}, - "isShowAgreeIconNew": {}, - "hecaiyundata2021jssdkcross": {}, - "random": {}, - "a_l2": {}, - "hecaiyun_stay_url": {}, - "a_l": {}, - "_139mail_login_type": {}, - "_139mail_login_shortAddr": {}, - "sajssdk_2015_cross_new_user": {}, - "fromhtml5": {}, - "html5SkinPath8011": {}, - "Os_SSo_Sid": {}, - "sid": {}, - "Login_UserNumber": {}, - "RMKEY": {}, - "rtexpired": {}, -} - type credentialState int const ( @@ -1195,28 +1174,6 @@ func mergeMailCookieHeader(existing string, responseCookies []*http.Cookie) stri return cookiepkg.ToString(cookies) } -func sanitizeMailLoginCookies(existing, newJSessionID string) string { - cookies := cookiepkg.Parse(existing) - filtered := cookies[:0] - for _, cookie := range cookies { - if _, excluded := mailLoginCookieExclusions[cookie.Name]; excluded { - continue - } - if cookie.Name == "JSESSIONID" { - if newJSessionID == "" { - continue - } - cookie.Value = newJSessionID - newJSessionID = "" - } - filtered = append(filtered, cookie) - } - if newJSessionID != "" { - filtered = append(filtered, &http.Cookie{Name: "JSESSIONID", Value: newJSessionID}) - } - return cookiepkg.ToString(filtered) -} - func mailRiskCode(location string) string { parsed, err := url.Parse(location) if err != nil { @@ -1267,13 +1224,6 @@ func mailXMLHeaders(cookie string) map[string]string { } } -func new139RestyClient() *resty.Client { - if base.RestyClient != nil { - return base.RestyClient.Clone() - } - return resty.New() -} - func (d *Yun139) sendSMSVerificationCode(riskCode string) error { scene, ok := smsSceneForRisk(riskCode) if !ok { @@ -1296,7 +1246,7 @@ func (d *Yun139) sendSMSVerificationCode(riskCode string) error { mailXMLField("scene", strconv.Itoa(scene)), "", }, "") - res, err := new139RestyClient().R(). + res, err := base.RestyClient.Clone().SetRetryCount(0).R(). SetHeaders(mailXMLHeaders(d.MailCookies)). SetBody(body). Post(mailSMSURL + "?func=" + url.QueryEscape("login:sendSmsCodeByScene") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10)) @@ -1349,7 +1299,7 @@ func (d *Yun139) verifySMSCode(riskCode string) (string, error) { pwdType, "", }, "") - res, err := new139RestyClient().R(). + res, err := base.RestyClient.Clone().SetRetryCount(0).R(). SetHeaders(mailXMLHeaders(d.MailCookies)). SetBody(body). Post(mailSMSURL + "?func=" + url.QueryEscape("/login/inlogin.action") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10)) @@ -1386,25 +1336,6 @@ func (d *Yun139) step1_password_login() (string, error) { log.Debugf("--- 执行步骤 1: 登录 API ---") loginURL := mailPasswordURL - preLogin, err := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy()).R().Get(mailRootURL) - if preLogin == nil { - return "", fmt.Errorf("step1 pre-login request failed: %v", err) - } - if err != nil && (preLogin.StatusCode() < 300 || preLogin.StatusCode() >= 400) { - return "", fmt.Errorf("step1 pre-login request failed: %w", err) - } - jsessionid := "" - for _, cookie := range preLogin.Cookies() { - if cookie.Name == "JSESSIONID" { - jsessionid = cookie.Value - break - } - } - if jsessionid == "" { - return "", errors.New("step1 pre-login response did not set JSESSIONID") - } - loginCookies := sanitizeMailLoginCookies(d.MailCookies, jsessionid) - // 密码 SHA1 哈希 hashedPassword := sha1Hash(fmt.Sprintf("fetion.com.cn:%s", d.Password)) @@ -1428,7 +1359,7 @@ func (d *Yun139) step1_password_login() (string, error) { "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0", - "Cookie": loginCookies, + "Cookie": d.MailCookies, } loginData := url.Values{} @@ -1444,31 +1375,25 @@ func (d *Yun139) step1_password_login() (string, error) { log.Debugf("DEBUG: 登录请求 URL: %s", loginURL) log.Debugf("DEBUG: 登录请求已准备") - // 设置客户端不跟随重定向 - client := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy()) - res, err := client.R(). + res, err := base.RestyClient.Clone(). + SetRetryCount(0). + SetRedirectPolicy(resty.RedirectPolicyFunc(func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + })).R(). SetHeaders(loginHeaders). SetFormDataFromValues(loginData). Post(loginURL) - if err != nil { - // 如果是重定向错误,则不作为失败处理,因为我们禁止了自动重定向 - if res != nil && res.StatusCode() >= 300 && res.StatusCode() < 400 { - log.Debugf("DEBUG: 登录响应 Status Code: %d (Redirect)", res.StatusCode()) - } else { - return "", fmt.Errorf("step1 login request failed: %w", err) - } - } else { - log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode()) + return "", fmt.Errorf("step1 login request failed: %w", err) } + log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode()) log.Debugf("DEBUG: 登录响应 Location present: %t", res.Header().Get("Location") != "") - var sid, extractedCguid string + d.MailCookies = mergeMailCookieHeader(d.MailCookies, res.Cookies()) - // 从 Location 头部提取 sid 和 cguid + sid := "" locationHeader := res.Header().Get("Location") if locationHeader != "" { - d.MailCookies = mergeMailCookieHeader(loginCookies, res.Cookies()) if riskCode := mailRiskCode(locationHeader); riskCode != "" { if _, ok := smsSceneForRisk(riskCode); !ok { return "", fmt.Errorf("139 Mail risk control triggered: %s", riskCode) @@ -1482,41 +1407,22 @@ func (d *Yun139) step1_password_login() (string, error) { } return d.verifySMSCode(riskCode) } - sidMatch := regexp.MustCompile(`sid=([^&]+)`).FindStringSubmatch(locationHeader) - cguidMatch := regexp.MustCompile(`cguid=([^&]+)`).FindStringSubmatch(locationHeader) - if len(sidMatch) > 1 { - sid = sidMatch[1] - log.Debugf("DEBUG: 从 Location 提取到 sid.") - } - if len(cguidMatch) > 1 { - extractedCguid = cguidMatch[1] - log.Debugf("DEBUG: 从 Location 提取到 cguid.") + if redirectURL, parseErr := url.Parse(locationHeader); parseErr == nil { + sid = redirectURL.Query().Get("sid") } } - // 如果 Location 中没有,尝试从 Set-Cookie 中提取 - if sid == "" || extractedCguid == "" { - setCookieHeaders := res.Header().Values("Set-Cookie") - for _, cookieStr := range setCookieHeaders { - ssoSidMatch := regexp.MustCompile(`Os_SSo_Sid=([^;]+)`).FindStringSubmatch(cookieStr) - cookieCguidMatch := regexp.MustCompile(`cguid=([^;]+)`).FindStringSubmatch(cookieStr) - if len(ssoSidMatch) > 1 && sid == "" { - sid = ssoSidMatch[1] - log.Debugf("DEBUG: 从 Set-Cookie 提取到 sid.") - } - if len(cookieCguidMatch) > 1 && extractedCguid == "" { - extractedCguid = cookieCguidMatch[1] - log.Debugf("DEBUG: 从 Set-Cookie 提取到 cguid.") + if sid == "" { + for _, cookie := range res.Cookies() { + if cookie.Name == "Os_SSo_Sid" || cookie.Name == "sid" { + sid = cookie.Value + break } } } - - if sid == "" || extractedCguid == "" { - return "", errors.New("failed to extract sid or cguid from login response") + if sid == "" { + return "", errors.New("failed to extract sid from login response") } - - d.MailCookies = mergeMailCookieHeader(loginCookies, res.Cookies()) - return sid, nil } @@ -1891,10 +1797,6 @@ func extractFastLoginCookies(mailCookies string) (sid string, rmkey string) { return sid, rmkey } -func isRedirectStatus(statusCode int) bool { - return statusCode >= 300 && statusCode <= 399 -} - func hasCookiePair(raw string) bool { for _, part := range strings.Split(raw, ";") { name, value, ok := strings.Cut(strings.TrimSpace(part), "=") @@ -1905,26 +1807,6 @@ func hasCookiePair(raw string) bool { return false } -func fetchMailJSessionID(endpoint string) (string, error) { - client := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy()) - res, err := client.R().Get(endpoint) - if res == nil { - return "", fmt.Errorf("pre-login request returned no response: %v", err) - } - if err != nil && !isRedirectStatus(res.StatusCode()) { - return "", fmt.Errorf("pre-login request failed with status %d: %w", res.StatusCode(), err) - } - if res.StatusCode() >= http.StatusBadRequest { - return "", fmt.Errorf("pre-login request failed with status %d", res.StatusCode()) - } - for _, cookie := range res.Cookies() { - if cookie.Name == "JSESSIONID" && cookie.Value != "" { - return cookie.Value, nil - } - } - return "", errors.New("pre-login response did not set JSESSIONID") -} - func (d *Yun139) tryFastLoginWithCookies() bool { sid, rmkey := extractFastLoginCookies(d.MailCookies) if sid == "" || rmkey == "" { @@ -1965,7 +1847,7 @@ func (d *Yun139) validateAndInitCredentials() error { return nil case credentialStateFullLogin, credentialStateCookiesOnly: log.Infof("139yun: Authorization missing, attempting login...") - if d.tryFastLoginWithCookies() { + if d.MailCookies != "" && d.tryFastLoginWithCookies() { return nil } @@ -2002,16 +1884,15 @@ func (d *Yun139) credentialState() (credentialState, error) { hasUsername := d.Username != "" hasPassword := strings.TrimSpace(d.Password) != "" - hasCookies := d.MailCookies != "" - if hasUsername || hasPassword { - if !hasUsername || !hasPassword || !hasCookies { - return 0, fmt.Errorf("if username or password is provided, all three (mail_cookies, username, password) must be provided") - } + if hasUsername != hasPassword { + return 0, fmt.Errorf("username and password must be provided together") + } + if hasUsername { return credentialStateFullLogin, nil } - if hasCookies { + if d.MailCookies != "" { return credentialStateCookiesOnly, nil } @@ -2019,8 +1900,8 @@ func (d *Yun139) credentialState() (credentialState, error) { } func (d *Yun139) loginWithPassword() (string, error) { - if d.Username == "" || d.Password == "" || d.MailCookies == "" { - return "", errors.New("username, password or mail_cookies is empty") + if d.Username == "" || d.Password == "" { + return "", errors.New("username or password is empty") } passId, err := d.step1_password_login() diff --git a/drivers/139/util_test.go b/drivers/139/util_test.go index 1080cd31f..d4abdc4d9 100644 --- a/drivers/139/util_test.go +++ b/drivers/139/util_test.go @@ -1,6 +1,7 @@ package _139 import ( + "fmt" "io" "net/http" "net/http/httptest" @@ -13,12 +14,13 @@ import ( "github.com/go-resty/resty/v2" ) -func TestSanitizeLoginCookiesDropsStaleJSessionIDWhenFreshOneMissing(t *testing.T) { - got := sanitizeMailLoginCookies("JSESSIONID=old; behaviorid=b", "") - want := "behaviorid=b" - if got != want { - t.Fatalf("sanitizeMailLoginCookies() = %q, want %q", got, want) - } +func useRetryingTestClient(t *testing.T) { + t.Helper() + oldClient := base.RestyClient + base.RestyClient = resty.New().SetRetryCount(3) + t.Cleanup(func() { + base.RestyClient = oldClient + }) } func TestMergeMailCookiesPreservesExistingOrderAndAppendsNewNames(t *testing.T) { @@ -61,6 +63,14 @@ func TestCredentialState(t *testing.T) { }}, want: credentialStateFullLogin, }, + { + name: "full login without initial cookies", + d: Yun139{Addition: Addition{ + Username: "user", + Password: "password", + }}, + want: credentialStateFullLogin, + }, { name: "cookies only", d: Yun139{Addition: Addition{MailCookies: "RMKEY=rm; Os_SSo_Sid=sid"}}, @@ -142,32 +152,31 @@ func TestIntegrationLoginObtainsAuthorization(t *testing.T) { t.Fatal("OPENLIST_139_MAIL_COOKIES is required") } - runFastLogin := func(t *testing.T, mailCookies string) { - t.Helper() + runFastLogin := func(mailCookies string) error { d := Yun139{Addition: Addition{MailCookies: mailCookies}} state, err := d.credentialState() if err != nil { - t.Fatalf("credentialState() unexpected error: %v", err) + return fmt.Errorf("credentialState() error: %w", err) } if state != credentialStateCookiesOnly { - t.Fatalf("credentialState() = %v, want cookies only", state) + return fmt.Errorf("credentialState() = %v, want cookies only", state) } sid, rmkey := extractFastLoginCookies(d.MailCookies) if sid == "" || rmkey == "" { - t.Fatal("mail cookies are missing Os_SSo_Sid or RMKEY") + return fmt.Errorf("mail cookies are missing Os_SSo_Sid or RMKEY") } token, err := d.step2_get_single_token(sid) if err != nil { - t.Fatalf("step2_get_single_token() error: %v", err) + return fmt.Errorf("step2_get_single_token() error: %w", err) } auth, err := d.step3_third_party_login(token) if err != nil { - t.Fatalf("step3_third_party_login() error: %v", err) + return fmt.Errorf("step3_third_party_login() error: %w", err) } - d.Authorization = auth - if d.Authorization == "" { - t.Fatal("authorization is empty after fast login") + if auth == "" { + return fmt.Errorf("authorization is empty after fast login") } + return nil } if username == "" || password == "" { @@ -231,44 +240,19 @@ func TestIntegrationLoginObtainsAuthorization(t *testing.T) { if sid == "" || rmkey == "" { t.Skip("input mail cookies are missing Os_SSo_Sid or RMKEY") } - runFastLogin(t, mailCookies) + if err := runFastLogin(mailCookies); err != nil { + t.Skipf("input mail cookies are stale or unusable: %v", err) + } }) t.Run("mail cookies fast login after password login", func(t *testing.T) { if refreshedMailCookies == "" { t.Fatal("password login did not refresh mail cookies") } - runFastLogin(t, refreshedMailCookies) - }) -} - -func TestIsRedirectStatus(t *testing.T) { - for _, status := range []int{300, 301, 302, 307, 399} { - if !isRedirectStatus(status) { - t.Fatalf("isRedirectStatus(%d) = false, want true", status) + if err := runFastLogin(refreshedMailCookies); err != nil { + t.Fatalf("refreshed mail cookies fast login failed: %v", err) } - } - for _, status := range []int{200, 299, 400, 500} { - if isRedirectStatus(status) { - t.Fatalf("isRedirectStatus(%d) = true, want false", status) - } - } -} - -func TestFetchMailJSessionIDAcceptsRedirectResponse(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{Name: "JSESSIONID", Value: "fresh"}) - http.Redirect(w, r, "/next", http.StatusFound) - })) - defer server.Close() - - got, err := fetchMailJSessionID(server.URL) - if err != nil { - t.Fatalf("fetchMailJSessionID() error: %v", err) - } - if got != "fresh" { - t.Fatalf("fetchMailJSessionID() = %q, want fresh", got) - } + }) } func TestInvalidAuthorizationDoesNotUseCookieFastLogin(t *testing.T) { @@ -304,18 +288,9 @@ func TestSMSSceneForRisk(t *testing.T) { } } -func TestSanitizeMailLoginCookiesKeepsDeviceContext(t *testing.T) { - got := sanitizeMailLoginCookies( - "behaviorid=device; Os_SSo_Sid=old-sid; RMKEY=old-rmkey; JSESSIONID=old-session; S_DEVICE_TOKEN=fingerprint", - "new-session", - ) - want := "behaviorid=device;JSESSIONID=new-session;S_DEVICE_TOKEN=fingerprint" - if got != want { - t.Fatalf("sanitizeMailLoginCookies() = %q, want %q", got, want) - } -} - func TestSendSMSVerificationCode(t *testing.T) { + useRetryingTestClient(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { @@ -349,6 +324,8 @@ func TestSendSMSVerificationCode(t *testing.T) { } func TestSendSMSVerificationCodeStopsAtPictureChallenge(t *testing.T) { + useRetryingTestClient(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"code":"PML401010021"}`) })) @@ -366,6 +343,8 @@ func TestSendSMSVerificationCodeStopsAtPictureChallenge(t *testing.T) { } func TestVerifySMSCode(t *testing.T) { + useRetryingTestClient(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { @@ -406,3 +385,90 @@ func TestVerifySMSCode(t *testing.T) { t.Fatalf("MailCookies = %q", d.MailCookies) } } + +func TestPasswordLoginWithoutInitialCookiesStopsAtRedirectAndPersistsCookies(t *testing.T) { + useRetryingTestClient(t) + + var loginRequests, redirectRequests int + var server *httptest.Server + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/Login/Login.ashx": + loginRequests++ + http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "new-rm"}) + http.SetCookie(w, &http.Cookie{Name: "Os_SSo_Sid", Value: "new-sid"}) + w.Header().Set("Location", server.URL+"/appmail?sid=single-sid") + w.WriteHeader(http.StatusFound) + case "/appmail": + redirectRequests++ + w.WriteHeader(http.StatusOK) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + oldURL := mailPasswordURL + mailPasswordURL = server.URL + "/Login/Login.ashx" + defer func() { mailPasswordURL = oldURL }() + + d := Yun139{Addition: Addition{Username: "18800000000", Password: "password"}} + sid, err := d.step1_password_login() + if err != nil || sid != "single-sid" { + t.Fatalf("sid=%q err=%v", sid, err) + } + if loginRequests != 1 || redirectRequests != 0 { + t.Fatalf("login/redirect requests=%d/%d, want 1/0", loginRequests, redirectRequests) + } + if !strings.Contains(d.MailCookies, "RMKEY=new-rm") || !strings.Contains(d.MailCookies, "Os_SSo_Sid=new-sid") { + t.Fatalf("response cookies were not persisted: %q", d.MailCookies) + } +} + +func TestPasswordLoginReusesRawRefreshedCookies(t *testing.T) { + useRetryingTestClient(t) + + var n int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + cookie := r.Header.Get("Cookie") + if !strings.Contains(cookie, "JSESSIONID=stale") || !strings.Contains(cookie, "behaviorid=device") { + t.Errorf("login %d lost raw cookie context: %q", n, cookie) + } + if n == 1 { + http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "rm-1"}) + http.SetCookie(w, &http.Cookie{Name: "Os_SSo_Sid", Value: "sid-1"}) + w.Header().Set("Location", "https://mail.10086.cn/?sid=first") + } else { + if !strings.Contains(cookie, "RMKEY=rm-1") || !strings.Contains(cookie, "Os_SSo_Sid=sid-1") { + t.Errorf("second login did not reuse first refreshed cookies: %q", cookie) + } + http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "rm-2"}) + http.SetCookie(w, &http.Cookie{Name: "Os_SSo_Sid", Value: "sid-2"}) + w.Header().Set("Location", "https://mail.10086.cn/?sid=second") + } + w.WriteHeader(http.StatusFound) + })) + defer server.Close() + + oldURL := mailPasswordURL + mailPasswordURL = server.URL + defer func() { mailPasswordURL = oldURL }() + + d := Yun139{Addition: Addition{Username: "18800000000", Password: "password", MailCookies: "Os_SSo_Sid=old; RMKEY=old; JSESSIONID=stale; behaviorid=device"}} + if sid, err := d.step1_password_login(); err != nil || sid != "first" { + t.Fatalf("first login sid=%q err=%v", sid, err) + } + refreshed := d.MailCookies + if !strings.Contains(refreshed, "RMKEY=rm-1") || !strings.Contains(refreshed, "Os_SSo_Sid=sid-1") { + t.Fatalf("first login did not persist refreshed cookies: %q", refreshed) + } + d.Authorization = "" + d.MailCookies = refreshed + if sid, err := d.step1_password_login(); err != nil || sid != "second" { + t.Fatalf("second login sid=%q err=%v", sid, err) + } + if n != 2 { + t.Fatalf("login count=%d, want 2", n) + } +} From 2a9e672194af0bc3fa33e93aaca5b96bad82ef0f Mon Sep 17 00:00:00 2001 From: UcnacDx2 <127503808+UcnacDx2@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:51:46 +0000 Subject: [PATCH 2/2] fix(drivers/139): guard mail login client initialization Fall back to base.NewRestyClient() when base.RestyClient has not been initialized, while preserving cloned global-client behavior and the login/SMS retry and redirect policies. --- drivers/139/util.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/139/util.go b/drivers/139/util.go index 44465db81..104a55b88 100644 --- a/drivers/139/util.go +++ b/drivers/139/util.go @@ -1224,6 +1224,13 @@ func mailXMLHeaders(cookie string) map[string]string { } } +func new139RestyClient() *resty.Client { + if base.RestyClient != nil { + return base.RestyClient.Clone() + } + return base.NewRestyClient() +} + func (d *Yun139) sendSMSVerificationCode(riskCode string) error { scene, ok := smsSceneForRisk(riskCode) if !ok { @@ -1246,7 +1253,7 @@ func (d *Yun139) sendSMSVerificationCode(riskCode string) error { mailXMLField("scene", strconv.Itoa(scene)), "", }, "") - res, err := base.RestyClient.Clone().SetRetryCount(0).R(). + res, err := new139RestyClient().SetRetryCount(0).R(). SetHeaders(mailXMLHeaders(d.MailCookies)). SetBody(body). Post(mailSMSURL + "?func=" + url.QueryEscape("login:sendSmsCodeByScene") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10)) @@ -1299,7 +1306,7 @@ func (d *Yun139) verifySMSCode(riskCode string) (string, error) { pwdType, "", }, "") - res, err := base.RestyClient.Clone().SetRetryCount(0).R(). + res, err := new139RestyClient().SetRetryCount(0).R(). SetHeaders(mailXMLHeaders(d.MailCookies)). SetBody(body). Post(mailSMSURL + "?func=" + url.QueryEscape("/login/inlogin.action") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10)) @@ -1375,7 +1382,7 @@ func (d *Yun139) step1_password_login() (string, error) { log.Debugf("DEBUG: 登录请求 URL: %s", loginURL) log.Debugf("DEBUG: 登录请求已准备") - res, err := base.RestyClient.Clone(). + res, err := new139RestyClient(). SetRetryCount(0). SetRedirectPolicy(resty.RedirectPolicyFunc(func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse