2026-03-03 11:27:07 -06:00
package main
import (
"crypto/tls"
"fmt"
"net/smtp"
"strings"
)
type SMTPConfig struct {
Host string
Port int
User string
Password string
From string
FromName string
}
// loadSMTPConfig reads SMTP settings from the config table, overlaying any
// values set via CLI flags (which take priority).
func ( app * App ) loadSMTPConfig ( ) SMTPConfig {
get := func ( key string ) string {
var v string
app . db . QueryRow ( ` SELECT value FROM config WHERE key = ? ` , key ) . Scan ( & v )
return v
}
cfg := SMTPConfig {
Host : app . smtpHost ,
Port : app . smtpPort ,
User : app . smtpUser ,
Password : app . smtpPassword ,
From : app . smtpFrom ,
FromName : app . smtpFromName ,
}
if cfg . Host == "" {
cfg . Host = get ( "smtp_host" )
}
if cfg . Port == 0 {
fmt . Sscanf ( get ( "smtp_port" ) , "%d" , & cfg . Port )
}
if cfg . User == "" {
cfg . User = get ( "smtp_user" )
}
if cfg . Password == "" {
cfg . Password = get ( "smtp_password" )
}
if cfg . From == "" {
cfg . From = get ( "smtp_from" )
}
if cfg . FromName == "" {
cfg . FromName = get ( "smtp_from_name" )
}
if cfg . Port == 0 {
cfg . Port = 587
}
return cfg
}
// sendEmail delivers a plain-text email.
// Uses implicit TLS on port 465, STARTTLS on all other ports.
func sendEmail ( cfg SMTPConfig , to , subject , body string ) error {
fromHeader := cfg . From
if cfg . FromName != "" {
fromHeader = fmt . Sprintf ( "%s <%s>" , cfg . FromName , cfg . From )
}
msg := fmt . Sprintf (
"From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s" ,
fromHeader , to , subject , body ,
)
addr := fmt . Sprintf ( "%s:%d" , cfg . Host , cfg . Port )
auth := smtp . PlainAuth ( "" , cfg . User , cfg . Password , cfg . Host )
if cfg . Port == 465 {
tlsCfg := & tls . Config { ServerName : cfg . Host }
conn , err := tls . Dial ( "tcp" , addr , tlsCfg )
if err != nil {
return fmt . Errorf ( "tls dial: %w" , err )
}
c , err := smtp . NewClient ( conn , cfg . Host )
if err != nil {
return fmt . Errorf ( "smtp client: %w" , err )
}
defer c . Close ( )
if err = c . Auth ( auth ) ; err != nil {
return fmt . Errorf ( "smtp auth: %w" , err )
}
if err = c . Mail ( cfg . From ) ; err != nil {
return fmt . Errorf ( "smtp mail from: %w" , err )
}
if err = c . Rcpt ( to ) ; err != nil {
return fmt . Errorf ( "smtp rcpt: %w" , err )
}
w , err := c . Data ( )
if err != nil {
return fmt . Errorf ( "smtp data: %w" , err )
}
if _ , err = fmt . Fprint ( w , msg ) ; err != nil {
return err
}
return w . Close ( )
}
return smtp . SendMail ( addr , auth , cfg . From , [ ] string { to } , [ ] byte ( msg ) )
}
2026-03-03 17:59:35 -06:00
func ( app * App ) resolveBaseURL ( ) string {
baseURL := app . baseURL
if baseURL == "" {
app . db . QueryRow ( ` SELECT value FROM config WHERE key = 'base_url' ` ) . Scan ( & baseURL )
}
return strings . TrimRight ( baseURL , "/" )
}
func ( app * App ) eventName ( ) string {
event , _ := app . getEvent ( )
if event != nil && event . Name != "" {
return event . Name
}
return "the event"
}
2026-03-04 10:53:42 -06:00
// sendTicketTokenEmail sends a volunteer token link for a ticket to its participant's email.
func ( app * App ) sendTicketTokenEmail ( tk Ticket ) error {
if tk . Code == nil || * tk . Code == "" {
return fmt . Errorf ( "ticket has no code" )
2026-03-03 11:27:07 -06:00
}
2026-03-04 10:53:42 -06:00
if tk . ParticipantID == nil {
return fmt . Errorf ( "ticket has no participant" )
}
p , err := app . getParticipant ( * tk . ParticipantID )
if err != nil || p == nil {
return fmt . Errorf ( "participant not found" )
}
if p . Email == "" {
return fmt . Errorf ( "participant has no email address" )
2026-03-03 11:27:07 -06:00
}
cfg := app . loadSMTPConfig ( )
2026-03-03 17:59:35 -06:00
eventName := app . eventName ( )
2026-03-04 10:53:42 -06:00
link := fmt . Sprintf ( "%s/v/%s" , app . resolveBaseURL ( ) , * tk . Code )
name := p . PreferredName
if name == "" {
name = tk . Name
}
2026-03-03 11:27:07 -06:00
subject := fmt . Sprintf ( "Your volunteer link for %s" , eventName )
body := fmt . Sprintf (
"Hi %s,\n\nThank you for volunteering at %s!\n\nYour volunteer token: %s\nYour signup link: %s\n\nUse this link to sign up for available shifts in your department.\n\nSee you there!\n" ,
2026-03-04 10:53:42 -06:00
name , eventName , * tk . Code , link ,
2026-03-03 11:27:07 -06:00
)
2026-03-04 10:53:42 -06:00
return sendEmail ( cfg , p . Email , subject , body )
2026-03-03 11:27:07 -06:00
}
2026-03-03 17:59:35 -06:00
func ( app * App ) sendConfirmationEmail ( to , name , confirmToken string ) error {
cfg := app . loadSMTPConfig ( )
eventName := app . eventName ( )
2026-03-03 19:55:35 -06:00
link := fmt . Sprintf ( "%s/confirm/%s" , app . resolveBaseURL ( ) , confirmToken )
2026-03-03 17:59:35 -06:00
subject := fmt . Sprintf ( "Please confirm your email for %s" , eventName )
body := fmt . Sprintf (
"Hi %s,\n\nThank you for signing up to volunteer at %s!\n\nPlease confirm your email address by visiting:\n%s\n\nIf you did not sign up, you can safely ignore this email.\n" ,
name , eventName , link ,
)
return sendEmail ( cfg , to , subject , body )
}
func ( app * App ) sendShiftSignupEmail ( to , name , kioskLink string ) error {
cfg := app . loadSMTPConfig ( )
eventName := app . eventName ( )
subject := fmt . Sprintf ( "Shift signups are open for %s!" , eventName )
body := fmt . Sprintf (
"Hi %s,\n\nShift signups are now open for %s!\n\nUse this link to sign up for available shifts:\n%s\n\nSee you there!\n" ,
name , eventName , kioskLink ,
)
return sendEmail ( cfg , to , subject , body )
}