package auth import ( "crypto/tls" "fmt" "net" "net/smtp" "strings" "time" ) // SMTPConfig holds SMTP server configuration type SMTPConfig struct { Host string Port int Username string Password string From string } // SMTPSender sends emails via SMTP type SMTPSender struct { config SMTPConfig } // NewSMTPSender creates a new SMTP sender with the given configuration func NewSMTPSender(config SMTPConfig) *SMTPSender { return &SMTPSender{config: config} } // heloHostname returns the hostname to use in HELO/EHLO. // Using the SMTP server host is generally safe; alternatively we could // use the sender's domain extracted from the From address. func heloHostname(from, smtpHost string) string { // Try to extract domain from the From address if at := strings.LastIndex(from, "@"); at >= 0 { domain := from[at+1:] // Only use it if it looks plausible (contains a dot) if strings.Contains(domain, ".") { return domain } } // Fallback to the SMTP host's domain (strip port if present) if host, _, err := net.SplitHostPort(smtpHost); err == nil { return host } return smtpHost } // sendMail is a custom replacement for smtp.SendMail that allows // setting the HELO/EHLO hostname explicitly. Some SMTP servers // reject messages when the HELO doesn't match a valid domain. func (s *SMTPSender) sendMail(to []string, msg []byte) error { addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port) helo := heloHostname(s.config.From, s.config.Host) // Connect to the SMTP server conn, err := net.DialTimeout("tcp", addr, 10*time.Second) if err != nil { return fmt.Errorf("failed to connect to SMTP server: %w", err) } defer conn.Close() // Create SMTP client client, err := smtp.NewClient(conn, s.config.Host) if err != nil { return fmt.Errorf("failed to create SMTP client: %w", err) } defer client.Close() // Send HELO/EHLO with the proper hostname if err := client.Hello(helo); err != nil { return fmt.Errorf("failed to send HELO: %w", err) } // Upgrade to TLS if supported (required for port 587 STARTTLS) // StartTLS internally re-sends EHLO per RFC 3207 after upgrading if ok, _ := client.Extension("STARTTLS"); ok { tlsConfig := &tls.Config{ ServerName: s.config.Host, } if err := client.StartTLS(tlsConfig); err != nil { return fmt.Errorf("STARTTLS upgrade failed: %w", err) } } // Authenticate auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host) if err := client.Auth(auth); err != nil { return fmt.Errorf("SMTP authentication failed: %w", err) } // Set sender if err := client.Mail(s.config.From); err != nil { return fmt.Errorf("failed to set sender: %w", err) } // Set recipients for _, recipient := range to { if err := client.Rcpt(recipient); err != nil { return fmt.Errorf("failed to set recipient %s: %w", recipient, err) } } // Send message body w, err := client.Data() if err != nil { return fmt.Errorf("failed to start data transfer: %w", err) } if _, err := w.Write(msg); err != nil { return fmt.Errorf("failed to write message body: %w", err) } if err := w.Close(); err != nil { return fmt.Errorf("failed to close message body: %w", err) } return client.Quit() } // SendOTP sends an OTP email to the recipient func (s *SMTPSender) SendOTP(to, otp string) error { subject := "Your inBOXER Login Code" body := fmt.Sprintf(`Your one-time password for inBOXER is: %s This code will expire in 10 minutes. If you didn't request this code, please ignore this email.`, otp) msg := []byte(fmt.Sprintf("From: %s\r\n"+ "To: %s\r\n"+ "Subject: %s\r\n"+ "\r\n"+ "%s\r\n", s.config.From, to, subject, body)) return s.sendMail([]string{to}, msg) } // SendWelcome sends a welcome email after successful registration func (s *SMTPSender) SendWelcome(to string) error { subject := "Welcome to inBOXER" body := `Welcome to inBOXER! Your email account has been successfully set up. You can now log in to the dashboard to configure your email settings and start organizing your inbox. Thank you for using inBOXER!` msg := []byte(fmt.Sprintf("From: %s\r\n"+ "To: %s\r\n"+ "Subject: %s\r\n"+ "\r\n"+ "%s\r\n", s.config.From, to, subject, body)) return s.sendMail([]string{to}, msg) } // ValidateConfig checks if SMTP configuration is valid func (s *SMTPConfig) ValidateConfig() error { var errors []string if s.Host == "" { errors = append(errors, "SMTP host is required") } if s.Port == 0 { errors = append(errors, "SMTP port is required") } if s.Username == "" { errors = append(errors, "SMTP username is required") } if s.Password == "" { errors = append(errors, "SMTP password is required") } if s.From == "" { errors = append(errors, "From address is required") } if len(errors) > 0 { return fmt.Errorf("invalid SMTP configuration: %s", strings.Join(errors, ", ")) } return nil }