Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions internal/conversation/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,90 @@ func (m *Manager) NotifyAssignment(userIDs []int, conversation models.Conversati
return nil
}

// NotifyAssignedUserOfIncomingEmail sends notifications to the currently assigned agent when a new inbound message arrives.
func (m *Manager) NotifyAssignedUserOfIncomingEmail(conversationUUID string, message models.Message) error {
conversation, err := m.GetConversation(0, conversationUUID, "")
if err != nil {
return fmt.Errorf("fetching conversation: %w", err)
}

if !conversation.AssignedUserID.Valid {
return nil
}

agent, err := m.userStore.GetAgent(conversation.AssignedUserID.Int, "")
if err != nil {
m.lo.Error("error fetching assigned agent for incoming email notification", "user_id", conversation.AssignedUserID.Int, "error", err)
return fmt.Errorf("fetching assigned agent: %w", err)
}

if agent.Email.String == "" {
m.lo.Debug("assigned agent has no email, sending in-app notification only", "user_id", agent.ID, "conversation_uuid", conversationUUID)
}

appRootURL := ""
if m.settingsStore != nil {
appRootURL, err = m.settingsStore.GetAppRootURL()
if err != nil {
m.lo.Error("error fetching app root url for incoming email notification", "conversation_uuid", conversationUUID, "error", err)
}
}

conversationURL := fmt.Sprintf("/inboxes/assigned/conversation/%s", conversation.UUID)
if appRootURL != "" {
conversationURL = fmt.Sprintf("%s/inboxes/assigned/conversation/%s", strings.TrimRight(appRootURL, "/"), conversation.UUID)
}

conversationSubject := conversation.Subject.String
if conversationSubject == "" {
conversationSubject = message.Subject
}
if conversationSubject == "" {
conversationSubject = "(no subject)"
}

messageBody := strings.TrimSpace(message.TextContent)
if messageBody == "" {
messageBody = strings.TrimSpace(message.Content)
}
if messageBody == "" {
messageBody = "(no message body)"
}

emailBody := fmt.Sprintf(
"Hi %s,\n\nA new customer email arrived for conversation #%s.\n\nCustomer: %s <%s>\nSubject: %s\n\nMessage:\n%s\n\nOpen conversation: %s\n",
agent.FirstName,
conversation.ReferenceNumber,
conversation.Contact.FullName(),
conversation.Contact.Email.String,
conversationSubject,
messageBody,
conversationURL,
)

emailSubject := fmt.Sprintf("New email on conversation #%s", conversation.ReferenceNumber)
var emailNotification *notifier.EmailNotification
if agent.Email.String != "" {
emailNotification = &notifier.EmailNotification{
Recipients: []string{agent.Email.String},
Subject: emailSubject,
Content: emailBody,
}
}

m.dispatcher.Send(notifier.Notification{
Type: nmodels.NotificationTypeAssignment,
RecipientIDs: []int{agent.ID},
Title: emailSubject,
Body: null.StringFrom(messageBody),
ConversationID: null.IntFrom(conversation.ID),
ConversationUUID: conversation.UUID,
Email: emailNotification,
})

return nil
}

// NotifyMention sends notifications (in-app, WebSocket, email) for mentions.
// For team mentions, expands to all team members.
func (m *Manager) NotifyMention(conversationUUID string, message models.Message, mentions []models.MentionInput, mentionedByUserID int) {
Expand Down
10 changes: 7 additions & 3 deletions internal/conversation/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,9 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
if err == nil {
m.webhookStore.TriggerEvent(wmodels.EventConversationCreated, conversation)
m.automation.EvaluateNewConversationRules(conversation)
if err := m.NotifyAssignedUserOfIncomingEmail(in.Message.ConversationUUID, in.Message); err != nil {
m.lo.Error("error sending incoming email notification", "conversation_uuid", in.Message.ConversationUUID, "error", err)
}
}
return nil
}
Expand Down Expand Up @@ -769,16 +772,17 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {

if conversation.SLAPolicyID.Int == 0 {
m.lo.Info("no SLA policy applied to conversation, skipping next response SLA event creation")
return nil
}
if deadline, err := m.slaStore.CreateNextResponseSLAEvent(conversation.ID, conversation.AppliedSLAID.Int, conversation.SLAPolicyID.Int, conversation.AssignedTeamID.Int); err != nil && !errors.Is(err, sla.ErrUnmetSLAEventAlreadyExists) {
} else if deadline, err := m.slaStore.CreateNextResponseSLAEvent(conversation.ID, conversation.AppliedSLAID.Int, conversation.SLAPolicyID.Int, conversation.AssignedTeamID.Int); err != nil && !errors.Is(err, sla.ErrUnmetSLAEventAlreadyExists) {
m.lo.Error("error creating next response SLA event", "conversation_id", conversation.ID, "error", err)
} else if !deadline.IsZero() {
m.lo.Info("next response SLA event created for conversation", "conversation_id", conversation.ID, "deadline", deadline, "sla_policy_id", conversation.SLAPolicyID.Int)
m.BroadcastConversationUpdate(in.Message.ConversationUUID, "next_response_deadline_at", deadline.Format(time.RFC3339))
// Clear next response met at timestamp as this event was just created.
m.BroadcastConversationUpdate(in.Message.ConversationUUID, "next_response_met_at", nil)
}
if err := m.NotifyAssignedUserOfIncomingEmail(in.Message.ConversationUUID, in.Message); err != nil {
m.lo.Error("error sending incoming email notification", "conversation_uuid", in.Message.ConversationUUID, "error", err)
}
}
return nil
}
Expand Down