Created Turnpike, event attendee and volunteer management

Built after prototype, Traverse, an attendee and volunteer list
maintainer.
This commit is contained in:
Pen Anderson 2026-03-03 11:27:07 -06:00
commit 5d56ba8112
59 changed files with 8663 additions and 0 deletions

140
email.go Normal file
View file

@ -0,0 +1,140 @@
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))
}
// sendTokenEmail sends a volunteer token link to the attendee's email address.
func (app *App) sendTokenEmail(a Attendee) error {
if a.Email == "" {
return fmt.Errorf("attendee has no email address")
}
if a.VolunteerToken == nil || *a.VolunteerToken == "" {
return fmt.Errorf("attendee has no volunteer token")
}
cfg := app.loadSMTPConfig()
baseURL := app.baseURL
if baseURL == "" {
app.db.QueryRow(`SELECT value FROM config WHERE key = 'base_url'`).Scan(&baseURL)
}
baseURL = strings.TrimRight(baseURL, "/")
event, _ := app.getEvent()
eventName := "the event"
if event != nil && event.Name != "" {
eventName = event.Name
}
link := fmt.Sprintf("%s/#/v/%s", baseURL, *a.VolunteerToken)
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",
a.Name, eventName, *a.VolunteerToken, link,
)
return sendEmail(cfg, a.Email, subject, body)
}