package email import ( "crypto/tls" "fmt" "mime" "net" "net/mail" "net/smtp" "strings" "time" "clapclap/internal/env" ) // IsValid returns true if s is a syntactically valid email address. // It uses net/mail to parse and rejects addresses with empty domain or no dot // in the domain. // It's fast, readable, and avoids brittle regexes. func IsValid(s string) bool { s = strings.TrimSpace(s) addr, err := mail.ParseAddress(s) if err != nil { return false } parts := strings.Split(addr.Address, "@") if len(parts) != 2 { return false } domain := parts[1] if domain == "" || !strings.Contains(domain, ".") { return false } return true } // DomainHasMX checks if domain has MX or A/AAAA records (network call). func DomainHasMX(domain string) bool { mx, err := net.LookupMX(domain) if err == nil && len(mx) > 0 { return true } // fallback to A/AAAA records addrs, err := net.LookupHost(domain) return err == nil && len(addrs) > 0 } type Email struct { To string Subject string Content string } // Send reads credentials from the environment and sends the email securely func Send(e Email) error { // 1. Check basic string syntax if !IsValid(e.To) { return fmt.Errorf("refusing to send: invalid email format provided '%s'", e.To) } // 2. Extract the raw address to prevent injection parsedAddr, _ := mail.ParseAddress(e.To) toAddr := parsedAddr.Address // 3. Verify the domain actually accepts emails parts := strings.Split(toAddr, "@") // Splits into ["detafe6383", "deapad.com"] domain := parts[1] // Grabs just "deapad.com" (which is a string) if !DomainHasMX(domain) { return fmt.Errorf("refusing to send: domain '%s' does not have valid mail servers", domain) } smtpHost := env.C.SMTP_HOST smtpPort := env.C.SMTP_PORT smtpUser := env.C.SMTP_USER smtpPass := env.C.SMTP_PASS // 2. Safely encode the subject (Fixes spam filter issues with Emojis/UTF-8) encodedSubject := mime.BEncoding.Encode("utf-8", e.Subject) // 3. Construct headers securely and in a PREDICTABLE ORDER var msg strings.Builder fmt.Fprintf(&msg, "From: %s\r\n", smtpUser) fmt.Fprintf(&msg, "To: %s\r\n", toAddr) fmt.Fprintf(&msg, "Subject: %s\r\n", encodedSubject) msg.WriteString("MIME-Version: 1.0\r\n") msg.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n") msg.WriteString("\r\n") msg.WriteString(e.Content) addr := net.JoinHostPort(smtpHost, smtpPort) var auth smtp.Auth if smtpUser != "" || smtpPass != "" { auth = smtp.PlainAuth("", smtpUser, smtpPass, smtpHost) } // 4. Implement network timeouts conn, err := net.DialTimeout("tcp", addr, 10*time.Second) if err != nil { return fmt.Errorf("smtp connection (with %v) timeout: %w", addr, err) } client, err := smtp.NewClient(conn, smtpHost) if err != nil { return fmt.Errorf("failed to create smtp client: %w", err) } defer client.Close() // 5. Try STARTTLS for security explicitly if auth != nil { if ok, _ := client.Extension("STARTTLS"); ok { config := &tls.Config{ServerName: smtpHost} if err = client.StartTLS(config); err != nil { return fmt.Errorf("failed to start TLS: %w", err) } } } // 6. Authenticate and send if auth != nil { if err = client.Auth(auth); err != nil { return fmt.Errorf("smtp authentication failed: %w", err) } } if smtpUser == "" { smtpUser = "no-reply@clap-clap.fun" } if err = client.Mail(smtpUser); err != nil { return err } if err = client.Rcpt(toAddr); err != nil { return err } w, err := client.Data() if err != nil { return err } _, err = w.Write([]byte(msg.String())) if err != nil { return err } if err = w.Close(); err != nil { return err } return client.Quit() }