package api
import (
"html/template"
"net/url"
"strings"
"time"
"craftdesign.group/quarry/internal/markup"
)
// mailCard is what one workflow's email says around its payload: the
// verb in the context line, what the recipient did to deserve it, and
// whether the workflow takes replies (it must agree with the reply flag
// on the workflow's email step, internal/notify/workflows.go).
type mailCard struct {
context func(from, repo, title string) template.HTML
why func(repo string) string
reply bool
body bool // the payload's text is rendered as the body
}
func strong(s string) string { return "<strong>" + template.HTMLEscapeString(s) + "</strong>" }
var mailCards = map[string]mailCard{
"quarry-mention": {
context: func(from, repo, _ string) template.HTML {
return template.HTML(strong(from) + " mentioned you in " + strong(repo))
},
why: func(repo string) string { return "You're getting this because you were mentioned in " + repo + "." },
reply: true, body: true,
},
"quarry-forum-reply": {
context: func(from, _, title string) template.HTML {
return template.HTML(strong(from) + " replied in " + strong(title))
},
why: func(repo string) string { return "You're getting this because you follow this thread in " + repo + "." },
reply: true, body: true,
},
"quarry-landing": {
context: func(from, repo, title string) template.HTML {
return template.HTML(strong(title))
},
why: func(repo string) string { return "You're getting this because it concerns you in " + repo + "." },
reply: true,
},
}
// mailOpen is the button's words, by the surface the link opens.
var mailOpen = map[string]string{
"chat": "Open the message", "team chat": "Open the chat", "forum": "Open the post",
"landing": "Open the landing", "ci": "Open the run",
}
func payloadStr(p map[string]any, k string) string {
s, _ := p[k].(string)
return s
}
// withMailHTML adds payload.html to an email workflow's event: the whole
// card (markup.EmailDocument) around the same rendering and sanitizing
// the pages use, made mail-safe. The text body stays as the workflow
// wrote it. It adds nothing without an origin (links could not be
// absolute) or when the document would be too large, and notify then
// sends text only. The payload is copied, like absoluteLink's.
func (d Deps) withMailHTML(e NotifyEvent, now time.Time) NotifyEvent {
if e.Workflow == "quarry-activity" && d.Origin != "" {
// The digest's card is written in notify (notify.Workflows), once
// for the batch; it names the forge through the payload.
p := make(map[string]any, len(e.Payload)+2)
for k, v := range e.Payload {
p[k] = v
}
p["origin"], p["host"] = d.link(""), d.originHost()
e.Payload = p
return e
}
c, ok := mailCards[e.Workflow]
link := payloadStr(e.Payload, "link")
if !ok || d.Origin == "" || !strings.HasPrefix(link, "http") {
return e
}
var body template.HTML
if c.body {
text := payloadStr(e.Payload, "text")
if text == "" {
return e
}
if body, ok = markup.EmailHTML([]byte(text), resolver{}, d.link); !ok {
return e
}
}
repo, surface := payloadStr(e.Payload, "repo"), payloadStr(e.Payload, "surface")
title := payloadStr(e.Payload, "title")
if e.Workflow == "quarry-landing" {
title = payloadStr(e.Payload, "summary")
}
meta := now.UTC().Format("Mon 2 Jan, 15:04 UTC")
if ref := payloadStr(e.Payload, "ref"); ref != "" {
meta += " ยท " + ref
}
doc := markup.EmailDoc{
Title: title,
Repo: repo,
Surface: surface,
Context: c.context(payloadStr(e.Payload, "from"), repo, title),
Meta: meta,
Body: body,
Open: mailOpen[surface], OpenURL: link,
Why: c.why(repo),
SettingsURL: d.link("/me#notifications"),
HomeURL: d.link(""), HomeName: d.originHost(),
}
if c.reply {
doc.ReplyIn = surface
}
h, ok := markup.EmailDocument(doc)
if !ok {
return e
}
p := make(map[string]any, len(e.Payload)+1)
for k, v := range e.Payload {
p[k] = v
}
p["html"] = h
e.Payload = p
return e
}
// originHost is the forge's host for display, falling back to the origin.
func (d Deps) originHost() string {
if u, err := url.Parse(d.Origin); err == nil && u.Host != "" {
return u.Host
}
return d.Origin
}
// accountCard wraps the body of a mail about the account itself: no repo,
// no preference to point at, and a button when the mail has one job.
func (d Deps) accountCard(subject string, body template.HTML, open, openURL string) string {
doc := markup.EmailDoc{
Title: subject, Surface: "account",
Context: template.HTML(strong(subject)),
Body: body, Open: open, OpenURL: openURL,
Why: "You're getting this because it is about your quarry account.",
}
if d.Origin != "" {
doc.HomeURL, doc.HomeName = d.link(""), d.originHost()
}
h, ok := markup.EmailDocument(doc)
if !ok {
return ""
}
return h
}