package auth import ( "bytes" "context" "crypto/tls" "fmt" "mime" "net" "net/mail" "net/smtp" "strings" "time" ) type SMTPOptions struct { Host string Port int Username string Password string UseTLS bool StartTLS bool InsecureSkipVerify bool Timeout time.Duration } type SMTPSender struct { opts SMTPOptions } func NewSMTPSender(opts SMTPOptions) (*SMTPSender, error) { opts.Host = strings.TrimSpace(opts.Host) if opts.Host == "" { return nil, fmt.Errorf("%w: smtp host is required", ErrProviderNotReady) } if opts.Port <= 0 { opts.Port = 587 } if opts.Timeout <= 0 { opts.Timeout = 10 * time.Second } return &SMTPSender{opts: opts}, nil } func (s *SMTPSender) SendLoginCode(ctx context.Context, message EmailMessage) error { if s == nil { return ErrProviderNotReady } if strings.TrimSpace(message.FromEmail) == "" || strings.TrimSpace(message.ToEmail) == "" { return fmt.Errorf("%w: email sender and recipient are required", ErrInvalidCredentials) } ctx, cancel := context.WithTimeout(ctx, s.opts.Timeout) defer cancel() client, err := s.smtpClient(ctx) if err != nil { return err } defer client.Close() if s.opts.StartTLS && !s.opts.UseTLS { if ok, _ := client.Extension("STARTTLS"); ok { if err := client.StartTLS(s.tlsConfig()); err != nil { return err } } } if strings.TrimSpace(s.opts.Username) != "" { auth := smtp.PlainAuth("", s.opts.Username, s.opts.Password, s.opts.Host) if err := client.Auth(auth); err != nil { return err } } if err := client.Mail(message.FromEmail); err != nil { return err } if err := client.Rcpt(message.ToEmail); err != nil { return err } writer, err := client.Data() if err != nil { return err } if _, err := writer.Write(buildSMTPMessage(message)); err != nil { _ = writer.Close() return err } if err := writer.Close(); err != nil { return err } return client.Quit() } func (s *SMTPSender) smtpClient(ctx context.Context) (*smtp.Client, error) { address := fmt.Sprintf("%s:%d", s.opts.Host, s.opts.Port) dialer := &net.Dialer{Timeout: s.opts.Timeout} conn, err := dialer.DialContext(ctx, "tcp", address) if err != nil { return nil, err } if s.opts.UseTLS { tlsConn := tls.Client(conn, s.tlsConfig()) if err := tlsConn.HandshakeContext(ctx); err != nil { _ = conn.Close() return nil, err } conn = tlsConn } return smtp.NewClient(conn, s.opts.Host) } func (s *SMTPSender) tlsConfig() *tls.Config { return &tls.Config{ ServerName: s.opts.Host, InsecureSkipVerify: s.opts.InsecureSkipVerify, } } func buildSMTPMessage(message EmailMessage) []byte { boundary := "moteva-auth-code" from := mail.Address{Name: message.FromName, Address: message.FromEmail} to := mail.Address{Address: message.ToEmail} var buffer bytes.Buffer buffer.WriteString("From: " + from.String() + "\r\n") buffer.WriteString("To: " + to.String() + "\r\n") buffer.WriteString("Subject: " + mime.QEncoding.Encode("utf-8", message.Subject) + "\r\n") buffer.WriteString("MIME-Version: 1.0\r\n") buffer.WriteString("Content-Type: multipart/alternative; boundary=" + boundary + "\r\n\r\n") buffer.WriteString("--" + boundary + "\r\n") buffer.WriteString("Content-Type: text/plain; charset=utf-8\r\n") buffer.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n") buffer.WriteString(message.Text + "\r\n\r\n") buffer.WriteString("--" + boundary + "\r\n") buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n") buffer.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n") buffer.WriteString(message.HTML + "\r\n\r\n") buffer.WriteString("--" + boundary + "--\r\n") return buffer.Bytes() }