postmoogle/vendor/github.com/etkecc/go-validator/v2/emails.go

207 lines
4.6 KiB
Go

package validator
import (
"context"
"errors"
"fmt"
"net"
"net/mail"
"strings"
"time"
"blitiri.com.ar/go/spf"
"github.com/etkecc/go-trysmtp"
)
var (
ErrSpamlist = fmt.Errorf("spamlist")
ErrNoMX = fmt.Errorf("no MX")
ErrSMTP = fmt.Errorf("SMTP")
ErrSPF = fmt.Errorf("SPF")
)
// Email checks if email is valid
// returnPath and optionalSenderIP are optional fields
func (v *V) Email(email, returnPath string, optionalSenderIP ...net.IP) bool {
return v.EmailWithError(email, returnPath, optionalSenderIP...) == nil
}
// EmailWithError checks if email is valid and returns an error if not
func (v *V) EmailWithError(email, returnPath string, optionalSenderIP ...net.IP) error {
// edge case: email may be optional
if email == "" {
if v.cfg.Email.Enforce {
return errors.New("email is required")
}
return nil
}
address, err := mail.ParseAddress(email)
if err != nil {
v.cfg.Log("email %s invalid, reason: %v", email, err)
return err
}
if returnPath != "" {
rpAddress, err := mail.ParseAddress(returnPath)
if err != nil {
v.cfg.Log("return path %s invalid, reason: %v", returnPath, err)
}
if rpAddress != nil {
returnPath = rpAddress.Address
}
}
email = address.Address
return v.emailChecks(email, returnPath, optionalSenderIP...)
}
func (v *V) emailChecks(email, returnPath string, optionalSenderIP ...net.IP) error {
maxChecks := 4
var senderIP net.IP
if len(optionalSenderIP) > 0 {
senderIP = optionalSenderIP[0]
}
errchan := make(chan error, maxChecks)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go v.emailSpamlist(ctx, email, errchan)
go v.emailNoMX(ctx, email, errchan)
go v.emailNoSPF(ctx, email, returnPath, senderIP, errchan)
go v.emailNoSMTP(ctx, email, errchan)
var checks int
for {
checks++
err := <-errchan
if err != nil {
v.cfg.Log("email %q is invalid, reason: %v", email, err)
return err
}
if checks >= maxChecks {
return nil
}
}
}
func (v *V) emailSpamlist(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
emailb := []byte(email)
for _, spamregex := range v.cfg.Email.spamlist {
if spamregex.Match(emailb) {
errchan <- ErrSpamlist
return
}
}
errchan <- nil
}
}
func (v *V) emailNoMX(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.cfg.Email.MX {
errchan <- nil
return
}
at := strings.LastIndex(email, "@")
domain := email[at+1:]
if !v.MX(domain) {
v.cfg.Log("email %s domain %s invalid, reason: no MX", email, domain)
errchan <- ErrNoMX
return
}
errchan <- nil
}
}
func (v *V) emailNoSMTP(ctx context.Context, email string, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.cfg.Email.SMTP {
errchan <- nil
return
}
trysmtp.SMTPAddrs = []string{":25"}
client, err := trysmtp.Connect(v.cfg.Email.From, email)
if err != nil {
if strings.HasPrefix(err.Error(), "45") {
v.cfg.Log("email %s may be invalid, reason: SMTP check (%v)", email, err)
errchan <- nil
return
}
v.cfg.Log("email %s invalid, reason: SMTP check (%v)", email, err)
errchan <- ErrSMTP
return
}
client.Close()
errchan <- nil
}
}
// shouldValidateReturnPath checks if returnPath should be validated in SPF check
func (v *V) shouldValidateReturnPath(email, returnPath string) bool {
if returnPath == "" {
return false
}
if email == returnPath {
return false
}
var emailDomain, returnPathDomain string
emailParts := strings.Split(email, "@")
if len(emailParts) < 2 {
return false
}
emailDomain = v.GetBase(emailParts[1])
if returnPath != "" {
returnPathParts := strings.Split(returnPath, "@")
if len(returnPathParts) < 2 {
return false
}
returnPathDomain = v.GetBase(returnPathParts[1])
}
return emailDomain == returnPathDomain
}
func (v *V) emailNoSPF(ctx context.Context, email, returnPath string, senderIP net.IP, errchan chan error) {
select {
case <-ctx.Done():
return
default:
if !v.cfg.Email.SPF {
errchan <- nil
return
}
opts := []spf.Option{
spf.WithContext(ctx),
spf.WithTraceFunc(v.cfg.Log),
}
resultEmail, _ := spf.CheckHostWithSender(senderIP, "", email, opts...) //nolint:errcheck // not a error
if resultEmail == spf.Fail {
if v.shouldValidateReturnPath(email, returnPath) {
resultReturnPath, _ := spf.CheckHostWithSender(senderIP, "", returnPath, opts...) //nolint:errcheck // not a error
if resultReturnPath == spf.Fail {
errchan <- ErrSPF
return
}
}
errchan <- ErrSPF
return
}
errchan <- nil
}
}