docs: update email templating docs with complete variable reference

- adds new `Author` template var and injects it into all templates
- make author fields empty for all automated system generated emails
This commit is contained in:
Abhinav Raut
2025-06-05 01:55:38 +05:30
parent 5d6897a960
commit ea0b7d6d52
6 changed files with 89 additions and 55 deletions

View File

@@ -1,6 +1,6 @@
# Templating
Templating in outgoing emails allows you to personalize content by embedding dynamic expressions like `{{ .Recipient.FullName }}`. These expressions reference fields from the conversation, contact, and recipient objects.
Templating in outgoing emails allows you to personalize content by embedding dynamic expressions like `{{ .Recipient.FullName }}`. These expressions reference fields from the conversation, contact, recipient, and author objects.
## Outgoing Email Template Expressions
@@ -8,36 +8,53 @@ If you want to customize the look of outgoing emails, you can do so in the Admin
### Conversation Variables
| Variable | Value |
| Variable | Value |
|---------------------------------|--------------------------------------------------------|
| {{ .Conversation.ReferenceNumber }} | The unique reference number of the conversation |
| {{ .Conversation.Subject }} | The subject of the conversation |
| {{ .Conversation.UUID }} | The unique identifier of the conversation |
| {{ .Conversation.ReferenceNumber }} | The unique reference number of the conversation |
| {{ .Conversation.Subject }} | The subject of the conversation |
| {{ .Conversation.Priority }} | The priority level of the conversation |
| {{ .Conversation.UUID }} | The unique identifier of the conversation |
### Contact Variables
| Variable | Value |
| Variable | Value |
|------------------------------|------------------------------------|
| {{ .Contact.FirstName }} | First name of the contact/customer |
| {{ .Contact.LastName }} | Last name of the contact/customer |
| {{ .Contact.FullName }} | Full name of the contact/customer |
| {{ .Contact.Email }} | Email address of the contact/customer |
| {{ .Contact.FirstName }} | First name of the contact/customer |
| {{ .Contact.LastName }} | Last name of the contact/customer |
| {{ .Contact.FullName }} | Full name of the contact/customer |
| {{ .Contact.Email }} | Email address of the contact/customer |
### Recipient Variables
| Variable | Value |
|--------------------------------|-----------------------------------|
| {{ .Recipient.FirstName }} | First name of the recipient |
| {{ .Recipient.LastName }} | Last name of the recipient |
| {{ .Recipient.FullName }} | Full name of the recipient |
| {{ .Recipient.Email }} | Email address of the recipient |
| Variable | Value |
|--------------------------------|-----------------------------------|
| {{ .Recipient.FirstName }} | First name of the recipient |
| {{ .Recipient.LastName }} | Last name of the recipient |
| {{ .Recipient.FullName }} | Full name of the recipient |
| {{ .Recipient.Email }} | Email address of the recipient |
### Author Variables
| Variable | Value |
|------------------------------|-----------------------------------|
| {{ .Author.FirstName }} | First name of the message author |
| {{ .Author.LastName }} | Last name of the message author |
| {{ .Author.FullName }} | Full name of the message author |
| {{ .Author.Email }} | Email address of the message author |
### Example outgoing email template
```html
Dear {{ .Recipient.FirstName }}
Dear {{ .Recipient.FirstName }},
{{ template "content" . }}
Best regards,
{{ .Author.FullName }}
---
Reference: {{ .Conversation.ReferenceNumber }}
```
Here, the `{{ template "content" . }}` serves as a placeholder for the body of the outgoing email. It will be replaced with the actual email content at the time of sending.
Similarly, the `{{ .Recipient.FirstName }}` expression will dynamically insert the recipient's first name when the email is sent.

View File

@@ -800,40 +800,30 @@ func (m *Manager) SendAssignedConversationEmail(userIDs []int, conversation mode
content, subject, err := m.template.RenderStoredEmailTemplate(template.TmplConversationAssigned,
map[string]any{
// Kept these lower case keys for backward compatibility.
"conversation": map[string]string{
"subject": conversation.Subject.String,
"uuid": conversation.UUID,
"reference_number": conversation.ReferenceNumber,
"priority": conversation.Priority.String,
},
"agent": map[string]string{
"full_name": agent.FullName(),
},
// Following the new structure.
"Conversation": map[string]any{
"ReferenceNumber": conversation.ReferenceNumber,
"Subject": conversation.Subject.String,
"Priority": conversation.Priority.String,
"UUID": conversation.UUID,
},
"Agent": map[string]any{
"FirstName": agent.FirstName,
"LastName": agent.LastName,
"FullName": agent.FullName(),
"Email": agent.Email,
},
"Contact": map[string]any{
"FirstName": conversation.Contact.FirstName,
"LastName": conversation.Contact.LastName,
"FullName": conversation.Contact.FullName(),
"Email": conversation.Contact.Email,
"Email": conversation.Contact.Email.String,
},
"Recipient": map[string]any{
"FirstName": agent.FirstName,
"LastName": agent.LastName,
"FullName": agent.FullName(),
"Email": agent.Email,
"Email": agent.Email.String,
},
// Automated messages do not have an author.
"Author": map[string]any{
"FirstName": "",
"LastName": "",
"FullName": "",
"Email": "",
},
})
if err != nil {
@@ -847,8 +837,8 @@ func (m *Manager) SendAssignedConversationEmail(userIDs []int, conversation mode
Provider: notifier.ProviderEmail,
}
if err := m.notifier.Send(nm); err != nil {
m.lo.Error("error sending notification message", "error", err)
return fmt.Errorf("sending notification message: %w", err)
m.lo.Error("error sending notification message", "template", template.TmplConversationAssigned, "conversation_uuid", conversation.UUID, "error", err)
return fmt.Errorf("sending notification message with template %s: %w", template.TmplConversationAssigned, err)
}
return nil
}

View File

@@ -144,7 +144,7 @@ func (m *Manager) sendOutgoingMessage(message models.Message) {
}
// Render content in template
if err := m.RenderContentInTemplate(inbox.Channel(), &message); err != nil {
if err := m.RenderMessageInTemplate(inbox.Channel(), &message); err != nil {
handleError(err, "error rendering content in template")
return
}
@@ -213,8 +213,8 @@ func (m *Manager) sendOutgoingMessage(message models.Message) {
}
}
// RenderContentInTemplate renders message content in template.
func (m *Manager) RenderContentInTemplate(channel string, message *models.Message) error {
// RenderMessageInTemplate renders message content in template.
func (m *Manager) RenderMessageInTemplate(channel string, message *models.Message) error {
switch channel {
case inbox.ChannelEmail:
conversation, err := m.GetConversation(0, message.ConversationUUID)
@@ -222,8 +222,14 @@ func (m *Manager) RenderContentInTemplate(channel string, message *models.Messag
m.lo.Error("error fetching conversation", "uuid", message.ConversationUUID, "error", err)
return fmt.Errorf("fetching conversation: %w", err)
}
// Pass conversation and contact data to the template for rendering any placeholders.
message.Content, err = m.template.RenderEmailWithTemplate(map[string]any{
sender, err := m.userStore.GetAgent(message.SenderID, "")
if err != nil {
m.lo.Error("error fetching message sender user", "sender_id", message.SenderID, "error", err)
return fmt.Errorf("fetching message sender user: %w", err)
}
data := map[string]any{
"Conversation": map[string]any{
"ReferenceNumber": conversation.ReferenceNumber,
"Subject": conversation.Subject.String,
@@ -234,15 +240,33 @@ func (m *Manager) RenderContentInTemplate(channel string, message *models.Messag
"FirstName": conversation.Contact.FirstName,
"LastName": conversation.Contact.LastName,
"FullName": conversation.Contact.FullName(),
"Email": conversation.Contact.Email,
"Email": conversation.Contact.Email.String,
},
"Recipient": map[string]any{
"FirstName": conversation.Contact.FirstName,
"LastName": conversation.Contact.LastName,
"FullName": conversation.Contact.FullName(),
"Email": conversation.Contact.Email,
"Email": conversation.Contact.Email.String,
},
}, message.Content)
"Author": map[string]any{
"FirstName": sender.FirstName,
"LastName": sender.LastName,
"FullName": sender.FullName(),
"Email": sender.Email.String,
},
}
// For automated replies set author fields to empty strings as the recipients will see name as System.
if sender.IsSystemUser() {
data["Author"] = map[string]any{
"FirstName": "",
"LastName": "",
"FullName": "",
"Email": "",
}
}
message.Content, err = m.template.RenderEmailWithTemplate(data, message.Content)
if err != nil {
m.lo.Error("could not render email content using template", "id", message.ID, "error", err)
return fmt.Errorf("could not render email content using template: %w", err)

View File

@@ -15,8 +15,6 @@ const (
// Message represents a message to be sent as a notification.
type Message struct {
// Recipients of the message
UserIDs []int
// Email addresses of the recipients
RecipientEmails []string
// Subject of the message

View File

@@ -674,18 +674,19 @@ func (m *Manager) SendNotification(scheduledNotification models.ScheduledSLANoti
"Priority": "",
"UUID": appliedSLA.ConversationUUID,
},
"Agent": map[string]any{
"FirstName": agent.FirstName,
"LastName": agent.LastName,
"FullName": agent.FullName(),
"Email": agent.Email,
},
"Recipient": map[string]any{
"FirstName": agent.FirstName,
"LastName": agent.LastName,
"FullName": agent.FullName(),
"Email": agent.Email,
},
// Automated emails do not have an author, so we set empty values.
"Author": map[string]any{
"FirstName": "",
"LastName": "",
"FullName": "",
"Email": "",
},
})
if err != nil {

View File

@@ -80,3 +80,7 @@ func (u *User) FullName() string {
func (u *User) HasAdminRole() bool {
return slices.Contains(u.Roles, rmodels.RoleAdmin)
}
func (u *User) IsSystemUser() bool {
return u.Email.String == SystemUserEmail
}