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)) } 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" } // 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() eventName := app.eventName() link := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *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) } func (app *App) sendConfirmationEmail(to, name, confirmToken string) error { cfg := app.loadSMTPConfig() eventName := app.eventName() link := fmt.Sprintf("%s/confirm/%s", app.resolveBaseURL(), confirmToken) 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) }