Add queued email notifications for collaboration events
This commit is contained in:
149
apps/server/internal/email/templates.go
Normal file
149
apps/server/internal/email/templates.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TemplateData carries the context required to render any notification email.
|
||||
// Fields are optional per template; callers should populate what they have.
|
||||
type TemplateData struct {
|
||||
ActorName string
|
||||
ActorEmail string
|
||||
DocumentPath string
|
||||
DocumentTitle string
|
||||
CommentBody string
|
||||
MentionText string
|
||||
ConflictDesc string
|
||||
ViewURL string
|
||||
UnsubURL string
|
||||
InstanceName string
|
||||
}
|
||||
|
||||
// Render returns a plain-text subject and body for the given notification
|
||||
// type. Subjects are short and prefixed with the document path so email
|
||||
// clients thread per-document.
|
||||
func Render(kind string, data TemplateData) (subject, body string, err error) {
|
||||
if data.InstanceName == "" {
|
||||
data.InstanceName = "Cairnquire"
|
||||
}
|
||||
switch kind {
|
||||
case "file_changed":
|
||||
return renderFileChanged(data)
|
||||
case "comment":
|
||||
return renderComment(data)
|
||||
case "mention":
|
||||
return renderMention(data)
|
||||
case "conflict":
|
||||
return renderConflict(data)
|
||||
default:
|
||||
return "", "", fmt.Errorf("unknown email template kind %q", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func subjectPrefix(data TemplateData) string {
|
||||
if data.DocumentPath != "" {
|
||||
return "[" + data.DocumentPath + "]"
|
||||
}
|
||||
return "[" + data.InstanceName + "]"
|
||||
}
|
||||
|
||||
func footer(data TemplateData) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("\n--\n")
|
||||
if data.ViewURL != "" {
|
||||
fmt.Fprintf(&b, "View online: %s\n", data.ViewURL)
|
||||
}
|
||||
if data.UnsubURL != "" {
|
||||
fmt.Fprintf(&b, "Unsubscribe: %s\n", data.UnsubURL)
|
||||
}
|
||||
fmt.Fprintf(&b, "You receive this because you watch this document on %s.\n", data.InstanceName)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func renderFileChanged(data TemplateData) (string, string, error) {
|
||||
title := data.DocumentTitle
|
||||
if title == "" {
|
||||
title = data.DocumentPath
|
||||
}
|
||||
actor := data.ActorName
|
||||
if actor == "" {
|
||||
actor = "Someone"
|
||||
}
|
||||
subject := fmt.Sprintf("%s Updated by %s", subjectPrefix(data), actor)
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s updated %q at %s UTC.\n", actor, title, time.Now().UTC().Format("2006-01-02 15:04"))
|
||||
if data.DocumentPath != "" {
|
||||
fmt.Fprintf(&b, "Path: %s\n", data.DocumentPath)
|
||||
}
|
||||
b.WriteString("\nView the changes online to see what is new.\n")
|
||||
b.WriteString(footer(data))
|
||||
return subject, b.String(), nil
|
||||
}
|
||||
|
||||
func renderComment(data TemplateData) (string, string, error) {
|
||||
actor := data.ActorName
|
||||
if actor == "" {
|
||||
actor = "Someone"
|
||||
}
|
||||
subject := fmt.Sprintf("%s Comment from %s", subjectPrefix(data), actor)
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s commented on %q:\n", actor, docLabel(data))
|
||||
b.WriteString("\n")
|
||||
if data.CommentBody != "" {
|
||||
// Quote the comment body, line by line, as plain text.
|
||||
for _, line := range strings.Split(data.CommentBody, "\n") {
|
||||
fmt.Fprintf(&b, "> %s\n", line)
|
||||
}
|
||||
} else {
|
||||
b.WriteString("> (no body)\n")
|
||||
}
|
||||
b.WriteString("\nReply to this email to respond.\n")
|
||||
b.WriteString(footer(data))
|
||||
return subject, b.String(), nil
|
||||
}
|
||||
|
||||
func renderMention(data TemplateData) (string, string, error) {
|
||||
actor := data.ActorName
|
||||
if actor == "" {
|
||||
actor = "Someone"
|
||||
}
|
||||
subject := fmt.Sprintf("%s Mention from %s", subjectPrefix(data), actor)
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s mentioned you on %q.\n", actor, docLabel(data))
|
||||
if data.MentionText != "" {
|
||||
b.WriteString("\n")
|
||||
for _, line := range strings.Split(data.MentionText, "\n") {
|
||||
fmt.Fprintf(&b, "> %s\n", line)
|
||||
}
|
||||
}
|
||||
b.WriteString(footer(data))
|
||||
return subject, b.String(), nil
|
||||
}
|
||||
|
||||
func renderConflict(data TemplateData) (string, string, error) {
|
||||
subject := fmt.Sprintf("%s Sync conflict", subjectPrefix(data))
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "A sync conflict was detected on %q.\n", docLabel(data))
|
||||
if data.ConflictDesc != "" {
|
||||
fmt.Fprintf(&b, "\nDetails: %s\n", data.ConflictDesc)
|
||||
}
|
||||
b.WriteString("\nOpen the document to review and resolve the conflict.\n")
|
||||
b.WriteString(footer(data))
|
||||
return subject, b.String(), nil
|
||||
}
|
||||
|
||||
func docLabel(data TemplateData) string {
|
||||
if data.DocumentTitle != "" {
|
||||
return data.DocumentTitle
|
||||
}
|
||||
if data.DocumentPath != "" {
|
||||
return data.DocumentPath
|
||||
}
|
||||
return "a document"
|
||||
}
|
||||
Reference in New Issue
Block a user