package mail import ( "crypto/rand" "encoding/hex" "fmt" "log" "net/smtp" "strings" "time" "wedding-server/internal/config" "wedding-server/internal/store" ) type Mailer struct { cfg config.EmailConfig } func New(cfg config.EmailConfig) *Mailer { return &Mailer{cfg: cfg} } // newMessageID builds an RFC 5322 Message-ID, scoped to the sender's own // domain so it satisfies mail servers that reject messages lacking one. func newMessageID(from string) string { domain := "localhost" if i := strings.LastIndex(from, "@"); i != -1 { domain = strings.TrimSuffix(from[i+1:], ">") } var buf [16]byte _, _ = rand.Read(buf[:]) return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), hex.EncodeToString(buf[:]), domain) } func (m *Mailer) send(subject, body string) error { if m.cfg.SMTPHost == "" { log.Printf("mail: SMTP not configured, skipping email %q", subject) return nil } addr := fmt.Sprintf("%s:%d", m.cfg.SMTPHost, m.cfg.SMTPPort) auth := smtp.PlainAuth("", m.cfg.SMTPUser, m.cfg.SMTPPass, m.cfg.SMTPHost) msg := fmt.Sprintf( "From: %s\r\nTo: %s\r\nSubject: %s\r\nMessage-ID: %s\r\nDate: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", m.cfg.From, m.cfg.To, subject, newMessageID(m.cfg.From), time.Now().Format(time.RFC1123Z), body, ) recipients := []string{m.cfg.To} for bcc := range strings.SplitSeq(m.cfg.Bcc, ",") { if bcc = strings.TrimSpace(bcc); bcc != "" { recipients = append(recipients, bcc) } } return smtp.SendMail(addr, auth, m.cfg.From, recipients, []byte(msg)) } func (m *Mailer) SendRSVP(r store.RSVP, allGuests []store.Guest) error { names := make([]string, len(r.Guests)) adults, children := 0, 0 for i, g := range r.Guests { if g.IsChild { names[i] = g.Name + " (child)" children++ } else { names[i] = g.Name adults++ } } joined := strings.Join(names, ", ") var b strings.Builder fmt.Fprintf(&b, "New RSVP for %d guest(s) — %d adult(s), %d child(ren): %s\n", len(r.Guests), adults, children, joined) if r.Message != "" { fmt.Fprintf(&b, "\nMessage:\n%s\n", r.Message) } if len(allGuests) > 0 { totalAdults, totalChildren := 0, 0 fmt.Fprintf(&b, "\nRSVPs so far (%d guest(s)):\n", len(allGuests)) for _, g := range allGuests { if g.IsChild { totalChildren++ fmt.Fprintf(&b, "- %s (child)\n", g.Name) } else { totalAdults++ fmt.Fprintf(&b, "- %s\n", g.Name) } } fmt.Fprintf(&b, "\n%d adult(s), %d child(ren) total\n", totalAdults, totalChildren) } return m.send(fmt.Sprintf("RSVP from %s", joined), b.String()) } func (m *Mailer) SendPhotoUpload(u store.PhotoUpload, files []store.PhotoFile) error { var b strings.Builder fmt.Fprintf(&b, "%s uploaded %d photo(s)\n\n", u.Name, len(files)) if u.Email != "" { fmt.Fprintf(&b, "Email: %s\n", u.Email) } if u.Phone != "" { fmt.Fprintf(&b, "Phone: %s\n", u.Phone) } if u.Message != "" { fmt.Fprintf(&b, "\nMessage:\n%s\n", u.Message) } b.WriteString("\nPhotos:\n") for _, f := range files { fmt.Fprintf(&b, "- %s\n", f.URL) } return m.send(fmt.Sprintf("%s uploaded photos", u.Name), b.String()) }