Skip to content

Commit 07b1850

Browse files
committed
fix: empty recipients in automated replies
- Make recipients list from the latest message recipients for automated replies
1 parent 66886c3 commit 07b1850

File tree

6 files changed

+173
-11
lines changed

6 files changed

+173
-11
lines changed

cmd/conversation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,8 @@ func handleCreateConversation(r *fastglue.Request) error {
707707
}
708708

709709
// Send reply to the created conversation.
710-
if err := app.conversation.SendReply(nil /**media**/, inboxID, auser.ID, conversationUUID, content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
711-
// Delete the conversation if sending the reply fails.
710+
if err := app.conversation.SendReply(nil /**media**/, inboxID, auser.ID /**sender_id**/, conversationUUID, content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
711+
// Delete the conversation if reply fails.
712712
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
713713
app.lo.Error("error deleting conversation", "error", err)
714714
}

internal/conversation/conversation.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ type queries struct {
213213
UnsnoozeAll *sqlx.Stmt `query:"unsnooze-all"`
214214
DeleteConversation *sqlx.Stmt `query:"delete-conversation"`
215215
RemoveConversationAssignee *sqlx.Stmt `query:"remove-conversation-assignee"`
216+
GetLatestMessage *sqlx.Stmt `query:"get-latest-message"`
216217

217218
// Dashboard queries.
218219
GetDashboardCharts string `query:"get-dashboard-charts"`
@@ -870,23 +871,53 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio
870871

871872
switch action.Type {
872873
case amodels.ActionAssignTeam:
873-
teamID, _ := strconv.Atoi(action.Value[0])
874+
teamID, err := strconv.Atoi(action.Value[0])
875+
if err != nil {
876+
return fmt.Errorf("invalid team ID %q: %w", action.Value[0], err)
877+
}
874878
return m.UpdateConversationTeamAssignee(conv.UUID, teamID, user)
875879
case amodels.ActionAssignUser:
876-
agentID, _ := strconv.Atoi(action.Value[0])
880+
agentID, err := strconv.Atoi(action.Value[0])
881+
if err != nil {
882+
return fmt.Errorf("invalid agent ID %q: %w", action.Value[0], err)
883+
}
877884
return m.UpdateConversationUserAssignee(conv.UUID, agentID, user)
878885
case amodels.ActionSetPriority:
879-
priorityID, _ := strconv.Atoi(action.Value[0])
886+
priorityID, err := strconv.Atoi(action.Value[0])
887+
if err != nil {
888+
return fmt.Errorf("invalid priority ID %q: %w", action.Value[0], err)
889+
}
880890
return m.UpdateConversationPriority(conv.UUID, priorityID, "", user)
881891
case amodels.ActionSetStatus:
882-
statusID, _ := strconv.Atoi(action.Value[0])
892+
statusID, err := strconv.Atoi(action.Value[0])
893+
if err != nil {
894+
return fmt.Errorf("invalid status ID %q: %w", action.Value[0], err)
895+
}
883896
return m.UpdateConversationStatus(conv.UUID, statusID, "", "", user)
884897
case amodels.ActionSendPrivateNote:
885898
return m.SendPrivateNote([]mmodels.Media{}, user.ID, conv.UUID, action.Value[0])
886899
case amodels.ActionReply:
887-
return m.SendReply([]mmodels.Media{}, conv.InboxID, user.ID, conv.UUID, action.Value[0], nil, nil, nil, nil)
900+
// Make recipient list.
901+
to, cc, bcc, err := m.makeRecipients(conv.ID, conv.Contact.Email.String, conv.InboxMail)
902+
if err != nil {
903+
return fmt.Errorf("making recipients for reply action: %w", err)
904+
}
905+
return m.SendReply(
906+
[]mmodels.Media{},
907+
conv.InboxID,
908+
user.ID,
909+
conv.UUID,
910+
action.Value[0],
911+
to,
912+
cc,
913+
bcc,
914+
map[string]any{}, /**meta**/
915+
)
888916
case amodels.ActionSetSLA:
889-
slaID, _ := strconv.Atoi(action.Value[0])
917+
slaID, err := strconv.Atoi(action.Value[0])
918+
if err != nil {
919+
return fmt.Errorf("invalid SLA ID %q: %w", action.Value[0], err)
920+
}
890921
return m.ApplySLA(conv, slaID, user)
891922
case amodels.ActionAddTags, amodels.ActionSetTags, amodels.ActionRemoveTags:
892923
return m.SetConversationTags(conv.UUID, action.Type, action.Value, user)
@@ -922,7 +953,14 @@ func (m *Manager) SendCSATReply(actorUserID int, conversation models.Conversatio
922953
meta := map[string]interface{}{
923954
"is_csat": true,
924955
}
925-
return m.SendReply([]mmodels.Media{}, conversation.InboxID, actorUserID, conversation.UUID, message, nil, nil, nil, meta)
956+
957+
// Make recipient list.
958+
to, cc, bcc, err := m.makeRecipients(conversation.ID, conversation.Contact.Email.String, conversation.InboxMail)
959+
if err != nil {
960+
return fmt.Errorf("making recipients for CSAT reply: %w", err)
961+
}
962+
963+
return m.SendReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta)
926964
}
927965

928966
// DeleteConversation deletes a conversation.

internal/conversation/message.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conver
321321
bcc = stringutil.RemoveEmpty(bcc)
322322

323323
if len(to) == 0 {
324-
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.empty", "name", "to"), nil)
324+
return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.empty", "name", "`to`"), nil)
325325
}
326326
meta["to"] = to
327327

@@ -841,3 +841,16 @@ func (m *Manager) uploadThumbnailForMedia(media mmodels.Media, content []byte) e
841841
}
842842
return nil
843843
}
844+
845+
// getLatestMessage returns the latest message in a conversation.
846+
func (m *Manager) getLatestMessage(conversationID int, typ []string, status []string, excludePrivate bool) (models.Message, error) {
847+
var message models.Message
848+
if err := m.q.GetLatestMessage.Get(&message, conversationID, pq.Array(typ), pq.Array(status), excludePrivate); err != nil {
849+
if err == sql.ErrNoRows {
850+
return message, sql.ErrNoRows
851+
}
852+
m.lo.Error("error fetching latest message from DB", "error", err)
853+
return message, fmt.Errorf("fetching latest message: %w", err)
854+
}
855+
return message, nil
856+
}

internal/conversation/queries.sql

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,4 +557,24 @@ WHERE
557557
)
558558

559559
-- name: delete-conversation
560-
DELETE FROM conversations WHERE uuid = $1;
560+
DELETE FROM conversations WHERE uuid = $1;
561+
562+
-- name: get-latest-message
563+
SELECT
564+
m.created_at,
565+
m.updated_at,
566+
m.status,
567+
m.type,
568+
m.content,
569+
m.uuid,
570+
m.private,
571+
m.sender_id,
572+
m.sender_type,
573+
m.meta
574+
FROM conversation_messages m
575+
WHERE m.conversation_id = $1
576+
AND m.type = ANY($2)
577+
AND m.status = ANY($3)
578+
AND m.private = NOT $4
579+
ORDER BY m.created_at DESC
580+
LIMIT 1;

internal/conversation/recipient.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package conversation
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/abhinavxd/libredesk/internal/conversation/models"
8+
"github.com/abhinavxd/libredesk/internal/stringutil"
9+
)
10+
11+
// makeRecipients computes the recipients for a given conversation ID using the last message in the conversation.
12+
func (m *Manager) makeRecipients(conversationID int, contactEmail, inboxEmail string) (to, cc, bcc []string, err error) {
13+
lastMessage, err := m.getLatestMessage(conversationID, []string{models.MessageIncoming, models.MessageOutgoing}, []string{models.MessageStatusReceived, models.MessageStatusSent}, true)
14+
if err != nil {
15+
return nil, nil, nil, fmt.Errorf("fetching message for makeRecipients: %w", err)
16+
}
17+
18+
var meta struct {
19+
From []string `json:"from"`
20+
To []string `json:"to"`
21+
CC []string `json:"cc"`
22+
BCC []string `json:"bcc"`
23+
}
24+
if err = json.Unmarshal(lastMessage.Meta, &meta); err != nil {
25+
return nil, nil, nil, err
26+
}
27+
28+
isIncoming := lastMessage.Type == models.MessageIncoming
29+
to, cc, bcc = stringutil.ComputeRecipients(
30+
meta.From, meta.To, meta.CC, meta.BCC, contactEmail, inboxEmail, isIncoming,
31+
)
32+
return
33+
}

internal/stringutil/stringutil.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/url"
1010
"path/filepath"
1111
"regexp"
12+
"slices"
1213
"strings"
1314
"time"
1415

@@ -200,3 +201,60 @@ func ExtractEmail(s string) (string, error) {
200201
}
201202
return addr.Address, nil
202203
}
204+
205+
// DedupAndExcludeString returns a deduplicated []string excluding empty and a specific value.
206+
func DedupAndExcludeString(list []string, exclude string) []string {
207+
seen := make(map[string]struct{}, len(list))
208+
cleaned := make([]string, 0, len(list))
209+
for _, s := range list {
210+
if s == "" || s == exclude {
211+
continue
212+
}
213+
if _, ok := seen[s]; !ok {
214+
seen[s] = struct{}{}
215+
cleaned = append(cleaned, s)
216+
}
217+
}
218+
return cleaned
219+
}
220+
221+
// ComputeRecipients deduplicates and resolves to, cc, and bcc fields.
222+
// Applies fallback logic using contactEmail and thread rules for incoming messages.
223+
func ComputeRecipients(
224+
from, to, cc, bcc []string,
225+
contactEmail, inboxEmail string,
226+
lastMessageIncoming bool,
227+
) (finalTo, finalCC, finalBCC []string) {
228+
if lastMessageIncoming {
229+
if len(from) > 0 {
230+
finalTo = from
231+
} else if contactEmail != "" {
232+
finalTo = []string{contactEmail}
233+
}
234+
} else {
235+
if len(to) > 0 {
236+
finalTo = to
237+
} else if contactEmail != "" {
238+
finalTo = []string{contactEmail}
239+
}
240+
}
241+
242+
finalCC = append([]string{}, cc...)
243+
244+
if lastMessageIncoming {
245+
if len(to) > 0 {
246+
finalCC = append(finalCC, to...)
247+
}
248+
if contactEmail != "" && !slices.Contains(finalTo, contactEmail) && !slices.Contains(finalCC, contactEmail) {
249+
finalCC = append(finalCC, contactEmail)
250+
}
251+
}
252+
253+
finalBCC = append([]string{}, bcc...)
254+
255+
finalTo = DedupAndExcludeString(finalTo, inboxEmail)
256+
finalCC = DedupAndExcludeString(finalCC, inboxEmail)
257+
finalBCC = DedupAndExcludeString(finalBCC, inboxEmail)
258+
259+
return
260+
}

0 commit comments

Comments
 (0)