// Copyright 2025 boop.cat // Licensed under the Apache License, Version 2.0 // See LICENSE file for details. package lib import ( "crypto/tls" "fmt" "net/smtp" "os" "time" ) func SendEmail(to, subject, body string) error { host := os.Getenv("SMTP_HOST") port := os.Getenv("SMTP_PORT") user := os.Getenv("SMTP_USER") pass := os.Getenv("SMTP_PASS") from := os.Getenv("SMTP_FROM") if from == "" { from = os.Getenv("MAIL_FROM") } if host == "" || from == "" { fmt.Printf("[Email] Skipped sending to %s (no config)\nSubject: %s\nBody: %s\n", to, subject, body) return nil } fromName := os.Getenv("SMTP_FROM_NAME") if fromName == "" { fromName = "boop.cat" } msg := []byte(fmt.Sprintf("From: %s <%s>\r\n"+ "To: %s\r\n"+ "Subject: %s\r\n"+ "MIME-Version: 1.0\r\n"+ "Content-Type: text/html; charset=\"UTF-8\"\r\n"+ "\r\n"+ "%s\r\n", fromName, from, to, subject, body)) addr := fmt.Sprintf("%s:%s", host, port) if port == "465" || os.Getenv("SMTP_SECURE") == "true" { tlsConfig := &tls.Config{ InsecureSkipVerify: false, ServerName: host, } conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { return fmt.Errorf("failed to dial tls: %w", err) } defer conn.Close() c, err := smtp.NewClient(conn, host) if err != nil { return fmt.Errorf("failed to create smtp client: %w", err) } defer c.Quit() if user != "" && pass != "" { auth := smtp.PlainAuth("", user, pass, host) if err = c.Auth(auth); err != nil { return fmt.Errorf("auth failed: %w", err) } } if err = c.Mail(from); err != nil { return err } if err = c.Rcpt(to); err != nil { return err } w, err := c.Data() if err != nil { return err } _, err = w.Write(msg) if err != nil { return err } err = w.Close() if err != nil { return err } return nil } auth := smtp.PlainAuth("", user, pass, host) err := smtp.SendMail(addr, auth, from, []string{to}, msg) if err != nil { fmt.Printf("[Email] Failed to send: %v\n", err) return err } return nil } func SendVerificationEmail(to, token, username string) error { displayName := username if displayName == "" { displayName = "there" } url := fmt.Sprintf("%s/auth/verify-email?token=%s", os.Getenv("PUBLIC_URL"), token) subject := "Verify your email - boop.cat" body := buildEmailTemplate("Verify your email", fmt.Sprintf("Hey %s! Click the button below to verify your email address and activate your account.", displayName), "Verify Email", url, "This link expires in 24 hours.") return SendEmail(to, subject, body) } func SendPasswordResetEmail(to, token, username string) error { displayName := username if displayName == "" { displayName = "there" } url := fmt.Sprintf("%s/reset-password?token=%s", os.Getenv("PUBLIC_URL"), token) subject := "Reset your password - boop.cat" body := buildEmailTemplate("Reset your password", fmt.Sprintf("Hey %s! Someone requested a password reset for your account. Click the button below to set a new password.", displayName), "Reset Password", url, "This link expires in 1 hour. If you didn't request this, ignore this email.") return SendEmail(to, subject, body) } func buildEmailTemplate(heading, message, buttonText, buttonURL, footer string) string { brandName := "boop.cat" return fmt.Sprintf(` %s `, heading, brandName, heading, message, buttonURL, buttonText, buttonURL, buttonURL, footer, time.Now().Year(), brandName) }