All configuration (including secrets) now lives in a single file: bin/config.yaml. The separate .env file has been eliminated. Changes: - config.go: Added SMTPSettings struct + AI.APIKey to Config; removed godotenv import, Environment struct, and all os.Getenv() calls - config.yaml: Added smtp section (host/port/username/password) and ai.api_key field with placeholder values - main.go: Reads SMTP and API key from cfg instead of env - smtp.go: Changed Port field from string to int - otp_test.go: Updated Port values to int - .env.example: Deleted (all config is in config.yaml) - .gitignore: Removed .env.example; kept .env for safety - go.mod/go.sum: Removed github.com/joho/godotenv dependency - install.sh: No longer creates .env or uses EnvironmentFile; warns about placeholder values in config.yaml instead
179 lines
No EOL
4.8 KiB
Go
179 lines
No EOL
4.8 KiB
Go
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
|
|
} |