Files
warmbly/internal/app/emailsend/forward_test.go
T

255 lines
8.6 KiB
Go

package emailsend
import (
"context"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/repository"
)
func forwardedFixture() *models.ForwardedMessage {
return &models.ForwardedMessage{
From: []string{"John Smith <john@example.com>"},
To: []string{"sales@example.com"},
CC: []string{"finance@example.com"},
Subject: "Pricing details",
Date: time.Date(2026, 9, 23, 8, 34, 0, 0, time.UTC),
BodyPlain: "Hi,\n\nHere are the pricing details...",
}
}
func TestRenderForwardedPlain(t *testing.T) {
loc, err := time.LoadLocation("Europe/Madrid")
if err != nil {
t.Skip("no tz database:", err)
}
_, plain := renderForwarded(forwardedFixture(), loc)
want := strings.Join([]string{
"---------- Forwarded message ---------",
"From: John Smith <john@example.com>",
"Date: Wed, Sep 23, 2026 at 10:34 AM CEST",
"Subject: Pricing details",
"To: sales@example.com",
"Cc: finance@example.com",
"",
"Hi,",
"",
"Here are the pricing details...",
}, "\n")
if plain != want {
t.Fatalf("plain forward:\n got %q\nwant %q", plain, want)
}
}
// The envelope is someone else's text going into our HTML, so it is escaped.
func TestRenderForwardedHTMLEscapesEnvelope(t *testing.T) {
m := forwardedFixture()
m.Subject = `<script>alert(1)</script>`
htmlBody, _ := renderForwarded(m, time.UTC)
if strings.Contains(htmlBody, "<script>") {
t.Fatalf("subject reached the HTML unescaped: %s", htmlBody)
}
for _, want := range []string{
"---------- Forwarded message ---------",
"From: John Smith &lt;john@example.com&gt;",
"Date: Wed, Sep 23, 2026 at 8:34 AM UTC",
"Cc: finance@example.com",
"Here are the pricing details...",
} {
if !strings.Contains(htmlBody, want) {
t.Fatalf("HTML forward is missing %q:\n%s", want, htmlBody)
}
}
}
// The original's HTML survives, sanitized, with its stylesheet folded inline.
func TestRenderForwardedKeepsSanitizedHTML(t *testing.T) {
m := forwardedFixture()
m.BodyHTML = `<html><head><style>.lead{color:#ff0000}</style></head><body bgcolor="#000000">` +
`<p class="lead">Quarterly <b>pricing</b></p><img src="x" onerror="alert(1)"><script>steal()</script></body></html>`
htmlBody, plain := renderForwarded(m, time.UTC)
if !strings.Contains(htmlBody, "<b>pricing</b>") {
t.Fatalf("original markup was flattened:\n%s", htmlBody)
}
if !strings.Contains(htmlBody, "color:#ff0000") && !strings.Contains(htmlBody, "color: #ff0000") {
t.Fatalf("original stylesheet was lost instead of inlined:\n%s", htmlBody)
}
for _, bad := range []string{"onerror", "<script", "steal()", "<style", "<body", "</body"} {
if strings.Contains(htmlBody, bad) {
t.Fatalf("unsafe markup %q survived:\n%s", bad, htmlBody)
}
}
if !strings.Contains(plain, "Here are the pricing details...") {
t.Fatalf("stored plain text should be the text part:\n%s", plain)
}
}
func TestRenderForwardedFallsBackBetweenParts(t *testing.T) {
m := forwardedFixture()
m.BodyPlain = ""
m.BodyHTML = `<p>Only <b>HTML</b> here</p>`
_, plain := renderForwarded(m, time.UTC)
if !strings.HasSuffix(plain, "Only HTML here") {
t.Fatalf("plain part should be derived from the HTML:\n%q", plain)
}
m = forwardedFixture()
m.CC = nil
htmlBody, plain := renderForwarded(m, time.UTC)
if !strings.Contains(htmlBody, "Here are the pricing details...") {
t.Fatalf("HTML part should be derived from the text:\n%s", htmlBody)
}
if strings.Contains(plain, "Cc:") {
t.Fatalf("an empty Cc should not be listed:\n%s", plain)
}
}
func TestForwardNoteFillsTheMissingPart(t *testing.T) {
h, p := forwardNote("", "FYI")
if !strings.Contains(h, "FYI") || p != "FYI" {
t.Fatalf("plain-only note: html %q plain %q", h, p)
}
h, p = forwardNote("<div>Take a <b>look</b></div>", "")
if h != "<div>Take a <b>look</b></div>" || p != "Take a look" {
t.Fatalf("html-only note: html %q plain %q", h, p)
}
// The composer's empty placeholder is no note.
h, _ = forwardNote("<div></div>", "Please review")
if !strings.Contains(h, "Please review") {
t.Fatalf("placeholder html kept instead of the note: %q", h)
}
h, p = forwardNote("", "")
if h != "" || p != "" {
t.Fatalf("an empty note must stay empty: html %q plain %q", h, p)
}
}
type fakeSendEmailRepo struct {
repository.EmailRepository
account *models.Email
}
func (f *fakeSendEmailRepo) GetByID(context.Context, uuid.UUID) (*models.Email, *errx.Error) {
return f.account, nil
}
type fakeSendTaskRepo struct {
repository.TaskRepository
stored *repository.EmailTask
}
func (f *fakeSendTaskRepo) CreateEmailTaskFull(_ context.Context, _ *repository.Task, et *repository.EmailTask) error {
f.stored = et
return nil
}
// The queued task carries the rendered message beside the note, in the
// mailbox's own time zone, and the note gains the part it was sent without.
func TestSendEmailStoresTheForwardedMessage(t *testing.T) {
orgID := uuid.New()
tasks := &fakeSendTaskRepo{}
svc := &emailSendService{
taskRepo: tasks,
emailRepo: &fakeSendEmailRepo{account: &models.Email{OrganizationID: &orgID, Timezone: "America/New_York"}},
}
_, xerr := svc.SendEmail(context.Background(), uuid.New(), orgID, uuid.New(), &SendEmailRequest{
To: []string{"new@example.com"}, Subject: "Fwd: Pricing details", BodyPlain: "FYI",
Forward: forwardedFixture(),
})
if xerr != nil {
t.Fatal(xerr)
}
et := tasks.stored
if et == nil {
t.Fatal("no task stored")
}
if et.BodyPlain != "FYI" || !strings.Contains(et.BodyHTML, "FYI") {
t.Fatalf("note: plain %q html %q", et.BodyPlain, et.BodyHTML)
}
if !strings.Contains(et.ForwardedPlain, "Date: Wed, Sep 23, 2026 at 4:34 AM EDT") ||
!strings.Contains(et.ForwardedHTML, "Here are the pricing details...") {
t.Fatalf("forwarded message: plain %q html %q", et.ForwardedPlain, et.ForwardedHTML)
}
// A plain reply stores no forward and keeps its body exactly.
_, xerr = svc.SendEmail(context.Background(), uuid.New(), orgID, uuid.New(), &SendEmailRequest{
To: []string{"them@example.com"}, Subject: "Re: x", BodyPlain: "Thanks",
})
if xerr != nil {
t.Fatal(xerr)
}
if et := tasks.stored; et.ForwardedHTML != "" || et.ForwardedPlain != "" || et.BodyHTML != "" || et.BodyPlain != "Thanks" {
t.Fatalf("reply changed: %+v", et)
}
}
type fakeTicketRepo struct {
repository.TrackedLinkRepository
links map[uuid.UUID]string
}
func (f *fakeTicketRepo) GetByID(_ context.Context, id uuid.UUID) (*repository.TrackedLink, error) {
if d, ok := f.links[id]; ok {
return &repository.TrackedLink{ID: id, Destination: d}, nil
}
return nil, nil
}
// A forwarded Warmbly send must not credit the new recipient's clicks to the
// original send, so its tickets go back to their destinations.
func TestUntrackForwardedRestoresDestinations(t *testing.T) {
known, unknown, script := uuid.New(), uuid.New(), uuid.New()
svc := &emailSendService{trackedLinkRepo: &fakeTicketRepo{links: map[uuid.UUID]string{
known: "https://acme.com/pricing?a=1&b=2",
script: "javascript:alert(1)",
}}}
ticket := func(id uuid.UUID) string { return "https://t.example.com/c/" + id.String() }
htmlIn := `<a href="` + ticket(known) + `">Pricing</a> <a href="` + ticket(unknown) + `">Old</a> <a href="` + ticket(script) + `">x</a>`
plainIn := "Pricing: " + ticket(known)
h, p := svc.untrackForwarded(context.Background(), htmlIn, plainIn)
if !strings.Contains(h, `href="https://acme.com/pricing?a=1&amp;b=2"`) || strings.Contains(h, ticket(known)) {
t.Fatalf("known ticket not restored in html:\n%s", h)
}
if p != "Pricing: https://acme.com/pricing?a=1&b=2" {
t.Fatalf("known ticket not restored in text: %q", p)
}
if !strings.Contains(h, ticket(unknown)) || !strings.Contains(h, ticket(script)) || strings.Contains(h, "javascript:") {
t.Fatalf("unknown or unsafe tickets must stay as they are:\n%s", h)
}
}
// An HTML-only send keeps its HTML verbatim and still ships a text part.
func TestSendEmailRendersTheTextPartOfAnHTMLOnlyBody(t *testing.T) {
orgID := uuid.New()
tasks := &fakeSendTaskRepo{}
svc := &emailSendService{
taskRepo: tasks,
emailRepo: &fakeSendEmailRepo{account: &models.Email{OrganizationID: &orgID}},
}
body := `<p>Here is our <a href="https://example.com/brochure.pdf">brochure</a>.</p>`
_, xerr := svc.SendEmail(context.Background(), uuid.New(), orgID, uuid.New(), &SendEmailRequest{
To: []string{"them@example.com"}, Subject: "Brochure", BodyHTML: body,
})
if xerr != nil {
t.Fatal(xerr)
}
et := tasks.stored
if et.BodyHTML != body {
t.Fatalf("html changed: %q", et.BodyHTML)
}
if et.BodyPlain != "Here is our brochure (https://example.com/brochure.pdf)." || et.Body != et.BodyPlain {
t.Fatalf("plain: %q body %q", et.BodyPlain, et.Body)
}
}