From 9e2034f5d7df2e7314c7ab719e3e5446d9b00ded Mon Sep 17 00:00:00 2001 From: Qingsheng Ma Date: Mon, 24 Aug 2026 15:14:54 +0800 Subject: [PATCH 1/2] feat!: remove non-functional Yahoo API verifier The Yahoo API verifier has been broken since ~2025-12: the endpoint it posted to, /account/module/create?validateField=userId, now returns 404. Its test panicked and, until #196, took the rest of the suite down with it. Investigation of Yahoo's current pages shows the underlying capability is gone, not merely relocated. Three independent flows were checked: - Signup, old per-field validation: endpoint is 404. - Signup, current page: a Next.js App Router single form POST to /account/create with no separate availability check; the only way to probe a username is to submit a full registration. - Forgot-password / find-username: also a single form POST guarded by a browser-fingerprint field, and it asks for a *recovery* email rather than the address under test. There is no public path left to determine whether a Yahoo address exists, so this follows the precedent of #113 (Gmail verifier removal). BREAKING CHANGE: EnableAPIVerifier(YAHOO) is removed. The exported YAHOO constant is gone and EnableAPIVerifier now returns an error for every vendor. The smtpAPIVerifier interface and the apiVerifiers dispatch remain as an extension point for future vendors. Verified with go build/vet, golangci-lint (0 issues), and the full suite (go test -race -covermode atomic): ok, 89.5% coverage, no panics. This is the first clean run since December. Refs #195 Co-Authored-By: Claude Opus 5 (1M context) --- smtp_by_api.go | 4 - smtp_by_api_yahoo.go | 181 -------------------------------------- smtp_by_api_yahoo_test.go | 47 ---------- smtp_test.go | 56 ------------ verifier.go | 18 ++-- 5 files changed, 8 insertions(+), 298 deletions(-) delete mode 100644 smtp_by_api_yahoo.go delete mode 100644 smtp_by_api_yahoo_test.go diff --git a/smtp_by_api.go b/smtp_by_api.go index 2bf2db1a..b2552ffb 100644 --- a/smtp_by_api.go +++ b/smtp_by_api.go @@ -1,9 +1,5 @@ package emailverifier -const ( - YAHOO = "yahoo" -) - type smtpAPIVerifier interface { // isSupported the specific host supports the check by api. isSupported(host string) bool diff --git a/smtp_by_api_yahoo.go b/smtp_by_api_yahoo.go deleted file mode 100644 index f11e4d85..00000000 --- a/smtp_by_api_yahoo.go +++ /dev/null @@ -1,181 +0,0 @@ -package emailverifier - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "net/http" - "regexp" - "strings" - "time" -) - -const ( - signupPage = "https://login.yahoo.com/account/create?specId=yidregsimplified&lang=en-US&src=&done=https%3A%2F%2Fwww.yahoo.com&display=login" - signupEndpoint = "https://login.yahoo.com/account/module/create?validateField=userId" - userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" -) - -// Check yahoo email exists by their login & registration page. -// See https://login.yahoo.com -// See https://login.yahoo.com/account/create -func newYahooAPIVerifier(client *http.Client) smtpAPIVerifier { - if client == nil { - client = http.DefaultClient - } - return yahoo{ - client: client, - } -} - -type yahoo struct { - client *http.Client -} - -type yahooValidateReq struct { - Domain, Username, Acrumb, SessionIndex string - Cookies []*http.Cookie -} - -type yahooErrorResp struct { - Errors []errItem `json:"errors"` -} - -type errItem struct { - Name string `json:"name"` - Error string `json:"error"` -} - -func (y yahoo) isSupported(host string) bool { - // FIXME Is this `contains` too lenient? - return strings.Contains(host, "yahoo") -} - -func (y yahoo) check(domain, username string) (*SMTP, error) { - cookies, signUpPageRespBytes, err := y.toSignUpPage() - if err != nil { - return nil, err - } - if len(cookies) == 0 { - return nil, errors.New("yahoo check by api, no cookies") - } - - acrumb := getAcrumb(cookies) - if acrumb == "" { - return nil, errors.New("yahoo check by api, no acrumb") - } - - sessionIndex := getSessionIndex(signUpPageRespBytes) - if sessionIndex == "" { - return nil, errors.New("yahoo check by api, no sessionIndex") - } - - yahooErrResp, err := y.sendValidateRequest(yahooValidateReq{ - Domain: domain, - Username: username, - Acrumb: acrumb, - SessionIndex: sessionIndex, - Cookies: cookies, - }) - if err != nil { - return nil, err - } - usernameExists := checkUsernameExists(yahooErrResp) - return &SMTP{ - HostExists: true, - Deliverable: usernameExists, - }, nil -} - -var sessionIndexPattern = regexp.MustCompile(`value="([^"]+)" name="sessionIndex"`) - -func getSessionIndex(respBytes []byte) string { - match := sessionIndexPattern.FindSubmatch(respBytes) - if len(match) > 1 { - return string(match[1]) - } - return "" -} - -var usernameExistsErrorPattern = regexp.MustCompile(`ERROR_1[0-9]{2}`) - -func checkUsernameExists(resp yahooErrorResp) bool { - for _, item := range resp.Errors { - if item.Name == "userId" && (item.Error == "IDENTIFIER_EXISTS" || usernameExistsErrorPattern.MatchString(item.Error)) { - return true - } - } - return false -} - -func (y yahoo) sendValidateRequest(req yahooValidateReq) (yahooErrorResp, error) { - var res yahooErrorResp - data, err := json.Marshal(struct { - Acrumb string `json:"acrumb"` - SpecId string `json:"specId"` - Yid string `json:"userId"` - SessionIndex string `json:"sessionIndex"` - YidDomain string `json:"yidDomain"` - }{ - Acrumb: req.Acrumb, - SpecId: "yidregsimplified", - Yid: req.Username, - SessionIndex: req.SessionIndex, - YidDomain: req.Domain, - }) - if err != nil { - return res, err - } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - request, err := http.NewRequestWithContext(ctx, http.MethodPost, signupEndpoint, bytes.NewReader(data)) - if err != nil { - return res, err - } - for _, c := range req.Cookies { - request.AddCookie(c) - } - request.Header.Add("X-Requested-With", "XMLHttpRequest") - request.Header.Add("Content-Type", "application/json; charset=UTF-8") - resp, err := y.client.Do(request) - if err != nil { - return res, err - } - defer resp.Body.Close() - respBytes, err := io.ReadAll(resp.Body) - if err != nil { - return res, err - } - return res, json.Unmarshal(respBytes, &res) -} - -func (y yahoo) toSignUpPage() ([]*http.Cookie, []byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - request, err := http.NewRequestWithContext(ctx, http.MethodGet, signupPage, nil) - if err != nil { - return nil, nil, err - } - request.Header.Add("User-Agent", userAgent) - resp, err := y.client.Do(request) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - respBytes, err := io.ReadAll(resp.Body) - return resp.Cookies(), respBytes, err -} - -var acrumbPattern = regexp.MustCompile(`s=(?P[^;^&]*)`) - -func getAcrumb(cookies []*http.Cookie) string { - for _, c := range cookies { - match := acrumbPattern.FindStringSubmatch(c.Value) - if len(match) > 1 { - return match[1] - } - } - return "" -} diff --git a/smtp_by_api_yahoo_test.go b/smtp_by_api_yahoo_test.go deleted file mode 100644 index e2ca9c1d..00000000 --- a/smtp_by_api_yahoo_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package emailverifier - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestYahooCheckByAPI(t *testing.T) { - yahooAPIVerifier := newYahooAPIVerifier(nil) - t.Run("email exists", func(tt *testing.T) { - res, err := yahooAPIVerifier.check("yahoo.com", "hello") - require.NoError(tt, err) - assert.True(tt, res.HostExists) - assert.True(tt, res.Deliverable) - }) - t.Run("invalid email not exists", func(tt *testing.T) { - res, err := yahooAPIVerifier.check("yahoo.com", "123") - require.NoError(tt, err) - assert.True(tt, res.HostExists) - assert.False(tt, res.Deliverable) - }) -} - -func TestGetAcrumb(t *testing.T) { - cookies0 := []*http.Cookie{ - {Value: "123321"}, - {Value: "v=1&s=gWKqrs5c&d=A6454c24b|Zt.ZFgb.2T"}, - } - acrumb := getAcrumb(cookies0) - assert.Equal(t, "gWKqrs5c", acrumb) - - cookies1 := []*http.Cookie{ - {Value: "123321"}, - {Value: "v=1&s=gWKqrs5c"}, - } - acrumb = getAcrumb(cookies1) - assert.Equal(t, "gWKqrs5c", acrumb) - - cookies2 := []*http.Cookie{ - {Value: "123321"}, - } - acrumb = getAcrumb(cookies2) - assert.Empty(t, acrumb) -} diff --git a/smtp_test.go b/smtp_test.go index 12a6332a..87a128c3 100644 --- a/smtp_test.go +++ b/smtp_test.go @@ -14,62 +14,6 @@ func TestCheckSMTPUnSupportedVendor(t *testing.T) { assert.Error(t, err) } -func TestCheckSMTPOK_ByApi(t *testing.T) { - cases := []struct { - name string - domain string - username string - expected *SMTP - }{ - { - name: "yahoo exists", - domain: "yahoo.com", - username: "someone", - expected: &SMTP{ - HostExists: true, - Deliverable: true, - }, - }, - { - name: "myyahoo exists", - domain: "myyahoo.com", - username: "someone", - expected: &SMTP{ - HostExists: true, - Deliverable: true, - }, - }, - { - name: "yahoo no exists", - domain: "yahoo.com", - username: "123", - expected: &SMTP{ - HostExists: true, - Deliverable: false, - }, - }, - { - name: "myyahoo no exists", - domain: "myyahoo.com", - username: "123", - expected: &SMTP{ - HostExists: true, - Deliverable: false, - }, - }, - } - _ = verifier.EnableAPIVerifier(YAHOO) - defer verifier.DisableAPIVerifier(YAHOO) - for _, c := range cases { - test := c - t.Run(test.name, func(tt *testing.T) { - smtp, err := verifier.CheckSMTP(test.domain, test.username) - assert.NoError(t, err) - assert.Equal(t, test.expected, smtp) - }) - } -} - func TestCheckSMTPOK_HostExists(t *testing.T) { domain := "github.com" diff --git a/verifier.go b/verifier.go index 0f849af3..28f3a8b8 100644 --- a/verifier.go +++ b/verifier.go @@ -2,7 +2,6 @@ package emailverifier import ( "fmt" - "net/http" "time" ) @@ -16,7 +15,7 @@ type Verifier struct { helloName string // email to use in the `MAIL FROM:` SMTP command. defaults to `localhost` schedule *schedule // schedule represents a job schedule proxyURI string // use a SOCKS5 proxy to verify the email, - apiVerifiers map[string]smtpAPIVerifier // currently support gmail & yahoo, further contributions are welcomed. + apiVerifiers map[string]smtpAPIVerifier // per-vendor API verifiers; no built-in vendors currently, contributions are welcomed. // Timeouts connectTimeout time.Duration // Timeout for establishing connections @@ -144,17 +143,16 @@ func (v *Verifier) EnableSMTPCheck() *Verifier { return v } -// EnableAPIVerifier API verifier is activated when EnableAPIVerifier for the target vendor. +// EnableAPIVerifier activates an API-based existence check for the given vendor. // ** Please know ** that this is a tricky way (but relatively stable) to check if target vendor's email exists. // If you use this feature in a production environment, please ensure that you have sufficient backup measures in place, as this may encounter rate limiting or other API issues. +// +// There are currently no built-in vendors: the Yahoo verifier was removed once its +// endpoint stopped existing (see https://github.com/AfterShip/email-verifier/issues/195), +// following the same fate as the Gmail verifier removed in #113. This remains an +// extension point; contributions adding a working vendor are welcome. func (v *Verifier) EnableAPIVerifier(name string) error { - switch name { - case YAHOO: - v.apiVerifiers[YAHOO] = newYahooAPIVerifier(http.DefaultClient) - default: - return fmt.Errorf("unsupported to enable the API verifier for vendor: %s", name) - } - return nil + return fmt.Errorf("unsupported to enable the API verifier for vendor: %s", name) } func (v *Verifier) DisableAPIVerifier(name string) { From ce5d2bd7eaa73e4c42c8fbb131a99d02f52191e3 Mon Sep 17 00:00:00 2001 From: Qingsheng Ma Date: Mon, 24 Aug 2026 15:42:52 +0800 Subject: [PATCH 2/2] docs: add changelog entries for the Yahoo verifier removal Also corrects the v1.4.0 entry's link text: it read #76 while pointing at pull/88. #88 is the Gmail/Yahoo API support PR; #76 is DisableCatchAllCheck, already referenced correctly under v1.3.3. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e211736d..f87e2d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ ## [Change log](https://github.com/AfterShip/email-verifier/releases) +Unreleased +---------- +* **Breaking**: Remove the non-functional Yahoo API verifier; `EnableAPIVerifier(YAHOO)` and the `YAHOO` constant are gone [#198](https://github.com/AfterShip/email-verifier/pull/198) +* Fix: Yahoo API test panic no longer aborts the test suite [#196](https://github.com/AfterShip/email-verifier/pull/196) + v1.4.0 ---------- -* Feature: Support Gmail&Yahoo SMTP check by API [#76](https://github.com/AfterShip/email-verifier/pull/88) +* Feature: Support Gmail&Yahoo SMTP check by API [#88](https://github.com/AfterShip/email-verifier/pull/88) * Optimization: Return HasMXRecord as true when at least one valid mx records exist [#94](https://github.com/AfterShip/email-verifier/pull/94) * Update Dependencies