package mail import ( "strings" "testing" "time" ) // Every template is parsed in init(), so a broken one panics the process at // boot rather than at send time. This test renders each of them with realistic // data, because parsing does not catch a field that does not exist on the data // a Send* method actually passes. func TestRenderAll(t *testing.T) { expires := time.Date(2026, 8, 14, 9, 0, 0, 0, time.UTC) cases := []struct { name string data any subject string wants []string }{ {"verification", struct { Link string TTLHours int }{"https://hq.example/verify?token=abc", 24}, "Verify your Vantage account", []string{"https://hq.example/verify?token=abc", "24 hours"}}, {"invite", struct{ AccountName, Link string }{ "Acme & Co", "https://hq.example/accept-invite?token=abc"}, "You have been invited to Acme & Co on Vantage", []string{"Acme & Co", "accept-invite"}}, {"license", struct{ InstanceName, Blob string }{"acme", "BLOB-123"}, "Your Vantage licence key", []string{"BLOB-123"}}, {"instanceready", struct { InstanceName, LoginURL string Expires time.Time }{"acme", "https://acme.vantage.example", expires}, "acme is ready", []string{"14 August 2026", "acme.vantage.example"}}, {"renewed", struct { InstanceName string Expires time.Time }{"acme", expires}, "acme renewed", []string{"14 August 2026"}}, {"expiring", struct { InstanceName, PortalURL string Expires time.Time }{"acme", "https://hq.example/billing", expires}, "acme expires on 14 August", []string{"hq.example/billing"}}, {"expired", struct { InstanceName, PortalURL string DeleteOn time.Time }{"acme", "https://hq.example/billing", expires}, "acme is now read-only", []string{"read-only", "14 August 2026"}}, {"deletionwarning", struct { InstanceName, PortalURL, When string DeleteOn time.Time }{"acme", "https://hq.example/billing", "tomorrow", expires}, "acme will be deleted tomorrow", []string{"cannot be undone"}}, {"linkreminder", struct{ InstanceName, PortalURL string }{ "acme", "https://hq.example"}, "Finish setting up acme", []string{"not linked yet"}}, {"cancelled", struct{ InstanceName string }{"acme"}, "Your Vantage subscription is cancelled", []string{"changes are disabled", "cancelled"}}, {"pastdue", struct{ InstanceName, PortalURL string }{"acme", "https://hq.example"}, "Payment failed for your Vantage subscription", []string{"retried"}}, {"monitoralert", MonitorEvent{ MonitorName: "api.example", Type: "http", OldStatus: "up", NewStatus: "down", Message: "connection refused", Time: expires, Down: true}, "[Vantage] api.example (http) is DOWN: connection refused", []string{"api.example", "connection refused", "2026-08-14"}}, {"contact", struct { Enquiry Received string }{Enquiry{Name: "Ada", Email: "ada@example.com", Servers: "10", Topic: "sales", Message: "hello"}, "now"}, "[Vantage] sales ada@example.com", []string{"ada@example.com", "hello"}}, } if len(cases) != len(sets) { t.Fatalf("%d templates on disk but %d covered", len(sets), len(cases)) } for _, c := range cases { t.Run(c.name, func(t *testing.T) { m, err := render(c.name, c.data) if err != nil { t.Fatalf("render: %v", err) } if m.Subject != c.subject { t.Errorf("subject = %q, want %q", m.Subject, c.subject) } if !strings.Contains(m.HTML, "Sent by Vantage") { t.Error("html body is missing the layout footer") } if !strings.Contains(m.HTML, "#071628") { t.Error("html body is not on the control plane ground") } if !strings.Contains(m.Text, "VANTAGE") { t.Error("text body is missing the layout header") } if strings.Contains(m.Text, "<") && strings.Contains(m.Text, "style=") { t.Error("markup leaked into the text part") } for _, want := range c.wants { if !strings.Contains(m.HTML, want) && !strings.Contains(m.Text, want) { t.Errorf("neither part contains %q", want) } } }) } } // A header value carrying CRLF must not be able to start a new header. func TestEnvelopeStripsHeaderInjection(t *testing.T) { s := Sender{Host: "localhost", Port: "587", From: "vantage@example.com"} b, err := s.envelope(message{ To: "someone@example.com\r\nBcc: attacker@example.com", Subject: "hello", Text: "body", HTML: "

body

", }) if err != nil { t.Fatalf("envelope: %v", err) } if strings.Contains(string(b), "\r\nBcc:") { t.Fatal("CRLF in a recipient produced an extra header") } } // Both parts must be present: a client that refuses HTML should not get a // blank message, and a text-only message scores worse with spam filters. func TestEnvelopeIsMultipartAlternative(t *testing.T) { s := Sender{Host: "localhost", Port: "587", From: "vantage@example.com"} b, err := s.envelope(message{To: "a@example.com", Subject: "s", Text: "TEXTBODY", HTML: "

HTMLBODY

"}) if err != nil { t.Fatalf("envelope: %v", err) } got := string(b) for _, want := range []string{ "Content-Type: multipart/alternative; boundary=", "Message-ID: <", "Date: ", "text/plain; charset=utf-8", "text/html; charset=utf-8", "TEXTBODY", "HTMLBODY", } { if !strings.Contains(got, want) { t.Errorf("envelope is missing %q", want) } } }