diff --git a/docs/docs/templating.md b/docs/docs/templating.md index 77d8e1a5..9ec399af 100644 --- a/docs/docs/templating.md +++ b/docs/docs/templating.md @@ -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. +Similarly, the `{{ .Recipient.FirstName }}` expression will dynamically insert the recipient's first name when the email is sent. \ No newline at end of file diff --git a/internal/conversation/conversation.go b/internal/conversation/conversation.go index 74a28f34..72abd2aa 100644 --- a/internal/conversation/conversation.go +++ b/internal/conversation/conversation.go @@ -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 } diff --git a/internal/conversation/message.go b/internal/conversation/message.go index 613a5862..111a68db 100644 --- a/internal/conversation/message.go +++ b/internal/conversation/message.go @@ -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) diff --git a/internal/notification/notification.go b/internal/notification/notification.go index 3c5fc40b..e85e184f 100644 --- a/internal/notification/notification.go +++ b/internal/notification/notification.go @@ -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 diff --git a/internal/sla/sla.go b/internal/sla/sla.go index cf7c2661..7db59cc7 100644 --- a/internal/sla/sla.go +++ b/internal/sla/sla.go @@ -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 { diff --git a/internal/user/models/models.go b/internal/user/models/models.go index 8f5b13da..f891d22f 100644 --- a/internal/user/models/models.go +++ b/internal/user/models/models.go @@ -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 +}