github.com/go-email-validator/go-email-validator@v0.0.0-20230409163946-b8b9e6a0552e/pkg/ev/evsmtp/smtp.go (about)

     1  package evsmtp
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/go-email-validator/go-email-validator/pkg/ev/evmail"
     8  	"github.com/go-email-validator/go-email-validator/pkg/ev/utils"
     9  	"github.com/go-email-validator/go-email-validator/pkg/log"
    10  	"github.com/modern-go/reflect2"
    11  	"github.com/sethvargo/go-password/password"
    12  	"github.com/tevino/abool"
    13  	"go.uber.org/zap"
    14  	"net"
    15  	"net/smtp"
    16  	"sync"
    17  )
    18  
    19  // Configuration constants
    20  const (
    21  	ErrPrefix        = "evsmtp: "
    22  	ErrConnectionMsg = ErrPrefix + "connection was not created"
    23  	DefaultEmail     = "user@example.org"
    24  	DefaultSMTPPort  = 25
    25  	DefaultHelloName = "localhost"
    26  )
    27  
    28  // MXs is short alias for []*net.MX
    29  type MXs = []*net.MX
    30  
    31  // Constants of stages
    32  const (
    33  	RandomRCPTStage = CloseStage + 1
    34  	ConnectionStage = RandomRCPTStage + 1
    35  )
    36  
    37  var (
    38  	// ErrConnection is error of connection
    39  	ErrConnection = NewError(ConnectionStage, errors.New(ErrConnectionMsg))
    40  	// DefaultFromEmail is address, used as default From email
    41  	DefaultFromEmail = evmail.FromString(DefaultEmail)
    42  )
    43  
    44  // Checker is SMTP validation
    45  type Checker interface {
    46  	Validate(mxs MXs, input Input) []error
    47  }
    48  
    49  // CheckerWithRandomRCPT is used for caching of RandomRCPT
    50  type CheckerWithRandomRCPT interface {
    51  	Checker
    52  	RandomRCPT
    53  }
    54  
    55  // RandomRCPTFunc is function for checking of Catching All
    56  type RandomRCPTFunc func(sm SendMail, email evmail.Address) (errs []error)
    57  
    58  // RandomRCPT Need to realize of is-a relation (inheritance)
    59  type RandomRCPT interface {
    60  	Call(sm SendMail, email evmail.Address) []error
    61  	set(fn RandomRCPTFunc)
    62  	get() RandomRCPTFunc
    63  }
    64  
    65  // RandomEmail is function type to generate random email for checking of Catching All emails by RCPTs
    66  type RandomEmail func(domain string) (evmail.Address, error)
    67  
    68  func randomEmail(domain string) (evmail.Address, error) {
    69  	gen, _ := password.NewGenerator(&password.GeneratorInput{
    70  		LowerLetters: password.LowerLetters + password.Digits,
    71  	})
    72  	username, err := gen.Generate(15, 0, 0, true, true)
    73  	return evmail.NewEmailAddress(username, domain), err
    74  }
    75  
    76  // CheckerDTO is DTO for NewChecker
    77  type CheckerDTO struct {
    78  	SendMailFactory SendMailDialerFactory
    79  	RandomEmail     RandomEmail
    80  	Options         Options
    81  }
    82  
    83  // NewChecker instantiates Checker
    84  func NewChecker(dto CheckerDTO) Checker {
    85  	if dto.SendMailFactory == nil {
    86  		dto.SendMailFactory = NewSendMailFactory(DirectDial, nil)
    87  	}
    88  
    89  	if dto.RandomEmail == nil {
    90  		dto.RandomEmail = randomEmail
    91  	}
    92  
    93  	if dto.Options == nil {
    94  		dto.Options = DefaultOptions()
    95  	}
    96  
    97  	opts := OptionsDTO{
    98  		EmailFrom:   evmail.EmptyEmail(dto.Options.EmailFrom(), DefaultFromEmail),
    99  		HelloName:   utils.DefaultString(dto.Options.HelloName(), DefaultHelloName),
   100  		Proxy:       dto.Options.Proxy(),
   101  		TimeoutCon:  dto.Options.TimeoutConnection(),
   102  		TimeoutResp: dto.Options.TimeoutResponse(),
   103  		Port:        utils.DefaultInt(dto.Options.Port(), DefaultSMTPPort),
   104  	}
   105  
   106  	c := checker{
   107  		sendMailFactory: dto.SendMailFactory,
   108  		Auth:            nil,
   109  		randomEmail:     dto.RandomEmail,
   110  		options:         NewOptions(opts),
   111  	}
   112  	c.RandomRCPT = &ARandomRCPT{fn: c.randomRCPT}
   113  
   114  	return c
   115  }
   116  
   117  /*
   118  Some SMTP server send additional message and we should read it
   119  2.1.5 for OK message
   120  */
   121  type checker struct {
   122  	RandomRCPT
   123  	sendMailFactory SendMailDialerFactory
   124  	Auth            smtp.Auth
   125  	randomEmail     RandomEmail
   126  	options         Options
   127  }
   128  
   129  type sendMailRWMutex struct {
   130  	m        sync.RWMutex
   131  	sendMail SendMail
   132  }
   133  
   134  func (s *sendMailRWMutex) Set(sendMail SendMail) *sendMailRWMutex {
   135  	s.m.Lock()
   136  	defer s.m.Unlock()
   137  
   138  	s.sendMail = sendMail
   139  
   140  	return s
   141  }
   142  
   143  func (s *sendMailRWMutex) Get() SendMail {
   144  	s.m.RLock()
   145  	defer s.m.RUnlock()
   146  	return s.sendMail
   147  }
   148  
   149  func (c checker) Validate(mxs MXs, input Input) (errs []error) {
   150  	var smMutex = &sendMailRWMutex{}
   151  	var err error
   152  	errs = make([]error, 0)
   153  	var host string
   154  
   155  	email := input.Email()
   156  	opts := NewOptions(OptionsDTO{
   157  		EmailFrom:   evmail.EmptyEmail(input.EmailFrom(), c.options.EmailFrom()),
   158  		HelloName:   utils.DefaultString(input.HelloName(), c.options.HelloName()),
   159  		Proxy:       utils.DefaultString(input.Proxy(), c.options.Proxy()),
   160  		TimeoutCon:  utils.DefaultDuration(input.TimeoutConnection(), c.options.TimeoutConnection()),
   161  		TimeoutResp: utils.DefaultDuration(input.TimeoutResponse(), c.options.TimeoutResponse()),
   162  		Port:        utils.DefaultInt(input.Port(), c.options.Port()),
   163  	})
   164  
   165  	for _, mx := range mxs {
   166  		host = fmt.Sprintf("%v:%v", mx.Host, opts.Port())
   167  
   168  		func() {
   169  			var cancel context.CancelFunc
   170  			var ctx context.Context
   171  			ctx = context.Background()
   172  			if opts.TimeoutConnection() > 0 {
   173  				// TODO think about logging of timeout connection error
   174  				ctx, cancel = context.WithTimeout(ctx, opts.TimeoutConnection())
   175  				defer cancel()
   176  			}
   177  
   178  			done := make(chan struct{}, 1)
   179  			go func() {
   180  				defer close(done)
   181  				var errSM error
   182  
   183  				sendMail, errSM := c.sendMailFactory(ctx, host, input)
   184  				if errSM == nil {
   185  					smMutex.Set(sendMail)
   186  				}
   187  			}()
   188  
   189  			select {
   190  			case <-ctx.Done():
   191  				return
   192  			case <-done:
   193  				return
   194  			}
   195  		}()
   196  
   197  		if reflect2.IsNil(smMutex.Get()) {
   198  			break
   199  		}
   200  	}
   201  
   202  	stage := SafeSendMailStage{SendMailStage: ConnectionStage}
   203  	sm := smMutex.Get()
   204  	if reflect2.IsNil(sm) {
   205  		return append(errs, ErrConnection)
   206  	}
   207  
   208  	needClose := abool.NewBool(true)
   209  	defer func() {
   210  		if needClose.IsNotSet() {
   211  			return
   212  		}
   213  		if err := sm.Close(); err != nil {
   214  			log.Logger().Error(fmt.Sprintf("sendMail.Close %v", err),
   215  				zap.String("email", email.String()),
   216  				zap.String("mxs", fmt.Sprint(mxs)),
   217  			)
   218  		}
   219  	}()
   220  
   221  	done := make(chan struct{}, 1)
   222  	isDone := abool.New()
   223  	errAppend := func(elems ...error) bool {
   224  		if isDone.IsNotSet() {
   225  			errs = append(errs, elems...)
   226  		}
   227  		return isDone.IsSet()
   228  	}
   229  
   230  	timeoutResponse := utils.DefaultDuration(input.TimeoutResponse(), c.options.TimeoutResponse())
   231  	ctx := context.Background()
   232  	if timeoutResponse > 0 {
   233  		var cancel context.CancelFunc
   234  		ctx, cancel = context.WithTimeout(ctx, timeoutResponse)
   235  		defer cancel()
   236  	}
   237  
   238  	go func() {
   239  		defer close(done)
   240  
   241  		stage.Set(HelloStage)
   242  		if err = sm.Hello(opts.HelloName()); err != nil {
   243  			errAppend(NewError(stage.Get(), err))
   244  			return
   245  		}
   246  
   247  		stage.Set(AuthStage)
   248  		if err = sm.Auth(c.Auth); err != nil {
   249  			errAppend(NewError(stage.Get(), err))
   250  			return
   251  		}
   252  
   253  		stage.Set(MailStage)
   254  		if err = sm.Mail(opts.EmailFrom().String()); err != nil {
   255  			errAppend(NewError(stage.Get(), err))
   256  			return
   257  		}
   258  
   259  		stage.Set(RandomRCPTStage)
   260  		if errsRandomRCPTs := c.RandomRCPT.Call(sm, email); len(errsRandomRCPTs) > 0 {
   261  			if errAppend(errsRandomRCPTs...) {
   262  				return
   263  			}
   264  			stage.Set(RCPTsStage)
   265  			if errsRCPTs := sm.RCPTs([]string{email.String()}); len(errsRCPTs) > 0 {
   266  				errAppend(NewError(stage.Get(), errsRCPTs[email.String()]))
   267  			}
   268  		}
   269  
   270  		stage.Set(QuitStage)
   271  		if err = sm.Quit(); err != nil {
   272  			errAppend(NewError(stage.Get(), err))
   273  		}
   274  		needClose.UnSet()
   275  	}()
   276  
   277  	defer isDone.Set()
   278  	select {
   279  	case <-ctx.Done():
   280  		errAppend(NewError(stage.Get(), ctx.Err()))
   281  		return
   282  	case <-done:
   283  		return
   284  	}
   285  }
   286  
   287  func (c checker) randomRCPT(sm SendMail, email evmail.Address) (errs []error) {
   288  	randomEmail, err := c.randomEmail(email.Domain())
   289  	if err != nil {
   290  		randomEmailErr := NewError(RandomRCPTStage, err)
   291  		log.Logger().Error(
   292  			fmt.Sprintf("generate random email: %v", randomEmailErr),
   293  			zap.String("email", email.String()),
   294  		)
   295  		return append(errs, randomEmailErr)
   296  	}
   297  
   298  	if errsRCPTs := sm.RCPTs([]string{randomEmail.String()}); len(errsRCPTs) > 0 {
   299  		return append(errs, NewError(RandomRCPTStage, errsRCPTs[randomEmail.String()]))
   300  	}
   301  
   302  	return
   303  }