package services import ( "bytes" "crypto/tls" "fmt" "html/template" "log" "net/smtp" "strings" "time" ) // EmailConfig holds SMTP configuration type EmailConfig struct { SMTPHost string SMTPPort string SMTPUser string SMTPPassword string FromEmail string ToEmail string } // EmailService handles email sending operations type EmailService struct { config *EmailConfig } // NewEmailService creates a new email service func NewEmailService(config *EmailConfig) *EmailService { return &EmailService{ config: config, } } // ContactFormData represents contact form submission data type ContactFormData struct { Email string Name string Company string Subject string Message string IP string Time time.Time } // Validate performs validation on contact form data func (c *ContactFormData) Validate() error { // Sanitize inputs c.Email = strings.TrimSpace(c.Email) c.Name = strings.TrimSpace(c.Name) c.Company = strings.TrimSpace(c.Company) c.Subject = strings.TrimSpace(c.Subject) c.Message = strings.TrimSpace(c.Message) // Required fields if c.Email == "" { return fmt.Errorf("email is required") } if c.Message == "" { return fmt.Errorf("message is required") } // Email format validation (basic) if !strings.Contains(c.Email, "@") || !strings.Contains(c.Email, ".") { return fmt.Errorf("invalid email format") } // Prevent email header injection if containsNewlines(c.Email) { return fmt.Errorf("invalid email: contains prohibited characters") } if containsNewlines(c.Subject) { return fmt.Errorf("invalid subject: contains prohibited characters") } // Length validation if len(c.Email) > 254 { return fmt.Errorf("email too long (max 254 characters)") } if len(c.Name) > 100 { return fmt.Errorf("name too long (max 100 characters)") } if len(c.Company) > 100 { return fmt.Errorf("company too long (max 100 characters)") } if len(c.Subject) > 200 { return fmt.Errorf("subject too long (max 200 characters)") } if len(c.Message) > 5000 { return fmt.Errorf("message too long (max 5000 characters)") } if len(c.Message) < 10 { return fmt.Errorf("message too short (min 10 characters)") } return nil } // containsNewlines checks for newline characters that could enable header injection func containsNewlines(s string) bool { return strings.ContainsAny(s, "\r\n") } // SendContactForm sends a contact form email func (e *EmailService) SendContactForm(data *ContactFormData) error { // Validate data if err := data.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Prepare email content subject := "[CV Contact] " if data.Subject != "" { subject += data.Subject } else { subject += "New Message" } // Build email body body, err := e.buildEmailBody(data) if err != nil { return fmt.Errorf("failed to build email body: %w", err) } // Send email if err := e.sendEmail(subject, body); err != nil { return fmt.Errorf("failed to send email: %w", err) } // Log successful send (without sensitive data) log.Printf("Contact form email sent successfully to %s from %s", e.config.ToEmail, data.Email) return nil } // buildEmailBody creates the email body from template func (e *EmailService) buildEmailBody(data *ContactFormData) (string, error) { emailTemplate := `New contact form submission: From: {{.Email}} Name: {{if .Name}}{{.Name}}{{else}}Not provided{{end}} Company: {{if .Company}}{{.Company}}{{else}}Not provided{{end}} Subject: {{if .Subject}}{{.Subject}}{{else}}Not provided{{end}} Message: {{.Message}} --- IP: {{.IP}} Time: {{.Time.Format "2006-01-02 15:04:05 MST"}} ` tmpl, err := template.New("contact").Parse(emailTemplate) if err != nil { return "", err } var body bytes.Buffer if err := tmpl.Execute(&body, data); err != nil { return "", err } return body.String(), nil } // sendEmail sends an email using SMTP func (e *EmailService) sendEmail(subject, body string) error { // Validate config if e.config.SMTPHost == "" || e.config.SMTPPort == "" { return fmt.Errorf("SMTP configuration incomplete") } if e.config.SMTPUser == "" || e.config.SMTPPassword == "" { return fmt.Errorf("SMTP credentials missing") } if e.config.ToEmail == "" { return fmt.Errorf("recipient email not configured") } // Build email message from := e.config.FromEmail if from == "" { from = e.config.SMTPUser } to := e.config.ToEmail message := e.formatMessage(from, to, subject, body) // SMTP server address addr := fmt.Sprintf("%s:%s", e.config.SMTPHost, e.config.SMTPPort) // Setup authentication auth := smtp.PlainAuth("", e.config.SMTPUser, e.config.SMTPPassword, e.config.SMTPHost) // Connect to SMTP server with TLS client, err := e.connectSMTP(addr) if err != nil { return fmt.Errorf("failed to connect to SMTP server: %w", err) } defer client.Close() // Authenticate if err = client.Auth(auth); err != nil { return fmt.Errorf("SMTP authentication failed: %w", err) } // Set sender and recipient if err = client.Mail(from); err != nil { return fmt.Errorf("failed to set sender: %w", err) } if err = client.Rcpt(to); err != nil { return fmt.Errorf("failed to set recipient: %w", err) } // Send message w, err := client.Data() if err != nil { return fmt.Errorf("failed to get data writer: %w", err) } _, err = w.Write([]byte(message)) if err != nil { return fmt.Errorf("failed to write message: %w", err) } err = w.Close() if err != nil { return fmt.Errorf("failed to close writer: %w", err) } return client.Quit() } // connectSMTP establishes an SMTP connection with TLS func (e *EmailService) connectSMTP(addr string) (*smtp.Client, error) { // Connect to server client, err := smtp.Dial(addr) if err != nil { return nil, err } // Start TLS tlsConfig := &tls.Config{ ServerName: e.config.SMTPHost, MinVersion: tls.VersionTLS12, } if err = client.StartTLS(tlsConfig); err != nil { client.Close() return nil, err } return client, nil } // formatMessage formats an email message with proper headers func (e *EmailService) formatMessage(from, to, subject, body string) string { headers := make(map[string]string) headers["From"] = from headers["To"] = to headers["Subject"] = subject headers["MIME-Version"] = "1.0" headers["Content-Type"] = "text/plain; charset=\"utf-8\"" headers["Date"] = time.Now().Format(time.RFC1123Z) var message strings.Builder for k, v := range headers { message.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) } message.WriteString("\r\n") message.WriteString(body) return message.String() }