137 lines
3.5 KiB
Go
137 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"net/smtp"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type smtpConfig struct {
|
|
Host string
|
|
Port int
|
|
User string
|
|
Password string
|
|
}
|
|
|
|
func getSMTPConfigFromEnv() smtpConfig {
|
|
port := 587
|
|
if rawPort := strings.TrimSpace(os.Getenv("STARTTLS_PORT")); rawPort != "" {
|
|
if p, err := strconv.Atoi(rawPort); err == nil {
|
|
port = p
|
|
}
|
|
}
|
|
return smtpConfig{
|
|
Host: strings.TrimSpace(os.Getenv("SMTP")),
|
|
Port: port,
|
|
User: strings.TrimSpace(os.Getenv("SMTP_USER")),
|
|
Password: strings.TrimSpace(os.Getenv("SMTP_PASSWORD")),
|
|
}
|
|
}
|
|
|
|
func (s smtpConfig) isConfigured() bool {
|
|
return s.Host != "" && s.User != "" && s.Password != "" && s.Port > 0
|
|
}
|
|
|
|
func getAppBaseURL() string {
|
|
base := strings.TrimSpace(os.Getenv("SITE_URL"))
|
|
if base == "" {
|
|
base = strings.TrimSpace(os.Getenv("APP_BASE_URL"))
|
|
}
|
|
if base == "" {
|
|
return "http://localhost:6081"
|
|
}
|
|
return strings.TrimRight(base, "/")
|
|
}
|
|
|
|
func sendPasswordResetEmail(config smtpConfig, recipientEmail, resetURL string) error {
|
|
body, err := renderTransactionalTemplate("password_reset.html", map[string]string{
|
|
"ActionURL": resetURL,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sendHTMLEmail(config, recipientEmail, "Bag Exchange - Password reset", body)
|
|
}
|
|
|
|
func sendEmailVerificationEmail(config smtpConfig, recipientEmail, verifyURL string) error {
|
|
body, err := renderTransactionalTemplate("verify_email.html", map[string]string{
|
|
"ActionURL": verifyURL,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sendHTMLEmail(config, recipientEmail, "Bag Exchange - Verify your email", body)
|
|
}
|
|
|
|
func sendHTMLEmail(config smtpConfig, recipientEmail, subject, htmlBody string) error {
|
|
if isDevEmailMode() {
|
|
return saveDevEmail(recipientEmail, subject, htmlBody)
|
|
}
|
|
|
|
auth := smtp.PlainAuth("", config.User, config.Password, config.Host)
|
|
message := "From: " + config.User + "\r\n" +
|
|
"To: " + recipientEmail + "\r\n" +
|
|
"Subject: " + subject + "\r\n" +
|
|
"MIME-Version: 1.0\r\n" +
|
|
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
|
|
htmlBody
|
|
return smtp.SendMail(fmt.Sprintf("%s:%d", config.Host, config.Port), auth, config.User, []string{recipientEmail}, []byte(message))
|
|
}
|
|
|
|
func renderTransactionalTemplate(templateName string, data any) (string, error) {
|
|
tmpl, err := template.ParseFiles(filepath.Join("templates", "transactionalMails", templateName))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var out bytes.Buffer
|
|
if err := tmpl.Execute(&out, data); err != nil {
|
|
return "", err
|
|
}
|
|
return out.String(), nil
|
|
}
|
|
|
|
func isDevEmailMode() bool {
|
|
return strings.EqualFold(strings.TrimSpace(os.Getenv("EMAIL_MODE")), "dev")
|
|
}
|
|
|
|
func saveDevEmail(recipientEmail, subject, htmlBody string) error {
|
|
const dir = "devEmails"
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
filename := fmt.Sprintf("%s_%s.html", time.Now().UTC().Format("20060102_150405"), sanitizeFilename(subject))
|
|
path := filepath.Join(dir, filename)
|
|
|
|
payload := "<!--\n" +
|
|
"To: " + recipientEmail + "\n" +
|
|
"Subject: " + subject + "\n" +
|
|
"GeneratedAtUTC: " + time.Now().UTC().Format(time.RFC3339) + "\n" +
|
|
"-->\n" + htmlBody
|
|
|
|
return os.WriteFile(path, []byte(payload), 0o644)
|
|
}
|
|
|
|
func sanitizeFilename(value string) string {
|
|
var b strings.Builder
|
|
for _, r := range strings.ToLower(strings.TrimSpace(value)) {
|
|
switch {
|
|
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
|
|
b.WriteRune(r)
|
|
case r == ' ' || r == '-' || r == '_':
|
|
b.WriteRune('_')
|
|
}
|
|
}
|
|
name := strings.Trim(b.String(), "_")
|
|
if name == "" {
|
|
return "email"
|
|
}
|
|
return name
|
|
}
|