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" }