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 }