// Package mail provides email sending functionality for password reset emails. // Uses Go's net/smtp package for sending emails via SMTP protocol. package mail import ( "crypto/rand" // cryptographically secure random number generator "crypto/tls" // TLS/SSL encryption for secure email sending "encoding/hex" // hex encoding for token generation "fmt" // string formatting for URLs and messages "net/smtp" // simple mail transfer protocol package "github.com/imc-vibe/backend/internal/config" ) // EmailService handles sending emails through SMTP. // Stores SMTP configuration and provides methods for different email types. type EmailService struct { host string // SMTP server hostname (e.g., "smtp.example.com") port string // SMTP server port (e.g., "587" for TLS, "465" for SSL) username string // SMTP authentication username password string // SMTP authentication password from string // Sender email address (e.g., "noreply@example.com") baseURL string // Base URL of the web application (for building reset links) } // NewEmailService creates a new EmailService instance from configuration. // cfg: application configuration containing SMTP settings and base URL. func NewEmailService(cfg *config.Config) *EmailService { return &EmailService{ host: cfg.SMTPHost, port: cfg.SMTPPort, username: cfg.SMTPUser, password: cfg.SMTPPassword, from: cfg.SMTPFrom, baseURL: cfg.BaseURL, } } // SendPasswordReset sends a password reset email to the user. // to: recipient email address // token: unique reset token that was generated and stored in the database // The email contains a clickable link to reset the password. func (s *EmailService) SendPasswordReset(to, token string) error { // Build the reset URL with the token as a query parameter. // Example: https://mail.example.com/auth/reset-password?token=abc123... resetURL := fmt.Sprintf("%s/auth/reset-password?token=%s", s.baseURL, token) // Compose the email subject and body. subject := "Password Reset Request" body := fmt.Sprintf(`Hi, You requested a password reset for your account. Click the link below to reset your password: %s This link will expire in 1 hour. If you didn't request this, please ignore this email. Best regards, IMC Vibe Mail Server Admin `, resetURL) // Send the email using the send method. return s.send(to, subject, body) } // send is the internal method that actually sends the email via SMTP. // Handles both port 587 (TLS) and port 465 (SSL) connections. func (s *EmailService) send(to, subject, body string) error { // Check if SMTP is configured. If not, return an error. if s.host == "" || s.username == "" { return fmt.Errorf("SMTP not configured") } // Build the server address in host:port format. addr := fmt.Sprintf("%s:%s", s.host, s.port) // Build the email message with headers. // \r\n is the standard line ending for email headers and body. msg := fmt.Sprintf("From: %s\r\n"+ "To: %s\r\n"+ "Subject: %s\r\n"+ "MIME-Version: 1.0\r\n"+ "Content-Type: text/plain; charset=\"UTF-8\"\r\n"+ "\r\n"+ "%s", s.from, to, subject, body) // Port 465 uses implicit SSL/TLS, port 587 uses STARTTLS. // Both are secure, but they work differently. var auth smtp.Auth if s.port == "465" { // For port 465, we need to establish a TLS connection first // before sending anything, then upgrade the connection. err := s.sendWithTLS(addr, to, msg) return err } else { // For other ports (usually 587), use STARTTLS which upgrades // a plain connection to TLS after connecting. auth = smtp.PlainAuth("", s.username, s.password, s.host) err := smtp.SendMail(addr, auth, s.from, []string{to}, []byte(msg)) return err } } // sendWithTLS sends an email using an explicit TLS connection on port 465. // Port 465 uses implicit TLS - the entire connection is encrypted from the start. func (s *EmailService) sendWithTLS(addr, to, msg string) error { // Create a TLS configuration for the secure connection. // ServerName must match the SMTP server's certificate. tlsConfig := &tls.Config{ ServerName: s.host, } // Establish a TLS connection to the SMTP server. // This creates an encrypted tunnel from the start. conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { return err } defer conn.Close() // Ensure connection is closed when we're done // Create an SMTP client on top of the TLS connection. client, err := smtp.NewClient(conn, s.host) if err != nil { return err } defer client.Close() // Authenticate with the SMTP server using PLAIN authentication. // The empty string "" is the identity (usually same as username). auth := smtp.PlainAuth("", s.username, s.password, s.host) if err := client.Auth(auth); err != nil { return err } // Set the sender (MAIL FROM command). if err := client.Mail(s.from); err != nil { return err } // Set the recipient (RCPT TO command). if err := client.Rcpt(to); err != nil { return err } // Start sending the message body (DATA command). w, err := client.Data() if err != nil { return err } // Write the email body to the data stream. _, err = w.Write([]byte(msg)) if err != nil { return err } // Close the data writer to finish the message. err = w.Close() if err != nil { return err } // Send the QUIT command to gracefully close the connection. return client.Quit() } // GenerateToken creates a cryptographically random token for password reset links. // Returns a 64-character hexadecimal string (32 bytes encoded as hex). // Uses crypto/rand for security-sensitive token generation. func GenerateToken() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic("crypto/rand failed: " + err.Error()) } return hex.EncodeToString(b) } // ValidateToken checks if a token has the correct format. // Returns true if the token is exactly 64 characters of valid hex. // This is a quick validation before checking the database. func ValidateToken(token string) bool { if len(token) != 64 { return false } _, err := hex.DecodeString(token) return err == nil }