// 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
|
%s
|
%s
%s
Or copy this link into your browser:
%s
|
|
|
© %d %s
|
|
`, heading, brandName, heading, message, buttonURL, buttonText, buttonURL, buttonURL, footer, time.Now().Year(), brandName)
}