Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: support gmail & yahoo smtp check by api #88

Merged
merged 15 commits into from
Jun 19, 2023
45 changes: 28 additions & 17 deletions smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/smtp"
"net/url"
"strings"
"sync"
"time"

Expand All @@ -33,26 +34,35 @@ func (v *Verifier) CheckSMTP(domain, username string) (*SMTP, error) {
}

var ret SMTP
var err error
email := fmt.Sprintf("%s@%s", username, domain)

// Dial any SMTP server that will accept a connection
client, err := newSMTPClient(domain, v.proxyURI)
client, mx, err := newSMTPClient(domain, v.proxyURI)
git-hulk marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return &ret, ParseSMTPError(err)
}

// Defer quit the SMTP connection
defer client.Close()

// Check by api when enabled and host recognized.
for _, apiVerifier := range v.apiVerifiers {
if apiVerifier.isSupported(strings.ToLower(mx.Host)) {
return apiVerifier.check(domain, username)
}
}

// Sets the HELO/EHLO hostname
if err := client.Hello(v.helloName); err != nil {
if err = client.Hello(v.helloName); err != nil {
return &ret, ParseSMTPError(err)
}

// Sets the from email
if err := client.Mail(v.fromEmail); err != nil {
if err = client.Mail(v.fromEmail); err != nil {
return &ret, ParseSMTPError(err)
}

// Defer quit the SMTP connection
defer client.Close()

// Host exists if we've successfully formed a connection
ret.HostExists = true

Expand All @@ -63,7 +73,7 @@ func (v *Verifier) CheckSMTP(domain, username string) (*SMTP, error) {
// Checks the deliver ability of a randomly generated address in
// order to verify the existence of a catch-all and etc.
randomEmail := GenerateRandomEmail(domain)
if err := client.Rcpt(randomEmail); err != nil {
if err = client.Rcpt(randomEmail); err != nil {
if e := ParseSMTPError(err); e != nil {
switch e.Message {
case ErrFullInbox:
Expand Down Expand Up @@ -94,27 +104,27 @@ func (v *Verifier) CheckSMTP(domain, username string) (*SMTP, error) {
return &ret, nil
}

email := fmt.Sprintf("%s@%s", username, domain)
if err := client.Rcpt(email); err == nil {
if err = client.Rcpt(email); err == nil {
ret.Deliverable = true
}

return &ret, nil
}

// newSMTPClient generates a new available SMTP client
func newSMTPClient(domain, proxyURI string) (*smtp.Client, error) {
func newSMTPClient(domain, proxyURI string) (*smtp.Client, *net.MX, error) {
domain = domainToASCII(domain)
mxRecords, err := net.LookupMX(domain)
if err != nil {
return nil, err
return nil, nil, err
}

if len(mxRecords) == 0 {
return nil, errors.New("No MX records found")
return nil, nil, errors.New("No MX records found")
}
// Create a channel for receiving response from
ch := make(chan interface{}, 1)
selectedMXCh := make(chan *net.MX, 1)

// Done indicates if we're still waiting on dial responses
var done bool
Expand All @@ -123,9 +133,9 @@ func newSMTPClient(domain, proxyURI string) (*smtp.Client, error) {
var mutex sync.Mutex

// Attempt to connect to all SMTP servers concurrently
for _, r := range mxRecords {
for i, r := range mxRecords {
addr := r.Host + smtpPort

index := i
go func() {
c, err := dialSMTP(addr, proxyURI)
if err != nil {
Expand All @@ -141,6 +151,7 @@ func newSMTPClient(domain, proxyURI string) (*smtp.Client, error) {
case !done:
done = true
ch <- c
selectedMXCh <- mxRecords[index]
default:
c.Close()
}
Expand All @@ -154,14 +165,14 @@ func newSMTPClient(domain, proxyURI string) (*smtp.Client, error) {
res := <-ch
switch r := res.(type) {
case *smtp.Client:
return r, nil
return r, <-selectedMXCh, nil
case error:
errs = append(errs, r)
if len(errs) == len(mxRecords) {
return nil, errs[0]
return nil, nil, errs[0]
}
default:
return nil, errors.New("Unexpected response dialing SMTP server")
return nil, nil, errors.New("Unexpected response dialing SMTP server")
}
}

Expand Down
13 changes: 13 additions & 0 deletions smtp_by_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package emailverifier

const (
GMAIL = "gmail"
YAHOO = "yahoo"
)

type smtpAPIVerifier interface {
// isSupported the specific host supports the check by api.
isSupported(host string) bool
// check must be called before isSupported == true
check(domain, username string) (*SMTP, error)
}
53 changes: 53 additions & 0 deletions smtp_by_api_gmail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package emailverifier

import (
"context"
"fmt"
"net/http"
"strings"
"time"
)

const (
glxuPageFormat = "https://mail.google.com/mail/gxlu?email=%s"
)

// See the link below to know why we can use this way to check if a gmail exists.
// https://blog.0day.rocks/abusing-gmail-to-get-previously-unlisted-e-mail-addresses-41544b62b2
func newGmailAPIVerifier(client *http.Client) smtpAPIVerifier {
if client == nil {
client = http.DefaultClient
}
return gmail{
client: client,
}
}

type gmail struct {
client *http.Client
}

func (g gmail) isSupported(host string) bool {
return strings.HasSuffix(host, ".google.com.")
}

func (g gmail) check(domain, username string) (*SMTP, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
email := fmt.Sprintf("%s@%s", username, domain)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(glxuPageFormat, email), nil)
if err != nil {
return nil, err
}
resp, err := g.client.Do(request)
if err != nil {
return &SMTP{}, err
}
defer resp.Body.Close()
emailExists := len(resp.Cookies()) > 0

return &SMTP{
HostExists: true,
Deliverable: emailExists,
}, nil
}
25 changes: 25 additions & 0 deletions smtp_by_api_gmail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package emailverifier

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGmailCheckByAPI(t *testing.T) {
gmailAPIVerifier := newGmailAPIVerifier(nil)

t.Run("email exists", func(tt *testing.T) {
res, err := gmailAPIVerifier.check("gmail.com", "someone")
assert.NoError(t, err)
assert.Equal(t, true, res.HostExists)
assert.Equal(t, true, res.Deliverable)
})
t.Run("invalid email not exists", func(tt *testing.T) {
// username must greater than 6 characters
res, err := gmailAPIVerifier.check("gmail.com", "hello")
assert.NoError(t, err)
assert.Equal(t, true, res.HostExists)
assert.Equal(t, false, res.Deliverable)
})
}
Loading
Loading