Skip to content

Commit f657a87

Browse files
authored
Merge pull request #85 from abhinavxd/fix/email-channel-to-bcc-cc
Fix and Improve Email Recipients Handling in Conversations
2 parents 88e07c3 + 6c9eca3 commit f657a87

File tree

21 files changed

+592
-238
lines changed

21 files changed

+592
-238
lines changed

cmd/conversation.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/abhinavxd/libredesk/internal/automation/models"
1212
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
1313
"github.com/abhinavxd/libredesk/internal/envelope"
14+
"github.com/abhinavxd/libredesk/internal/stringutil"
1415
umodels "github.com/abhinavxd/libredesk/internal/user/models"
1516
"github.com/valyala/fasthttp"
1617
"github.com/volatiletech/null/v9"
@@ -640,24 +641,29 @@ func handleCreateConversation(r *fastglue.Request) error {
640641
firstName = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("first_name")))
641642
lastName = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("last_name")))
642643
subject = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("subject")))
643-
content = string(r.RequestCtx.PostArgs().Peek("content"))
644+
content = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("content")))
645+
to = []string{email}
644646
)
647+
645648
// Validate required fields
646649
if inboxID <= 0 {
647650
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`inbox_id`"), nil, envelope.InputError)
648651
}
649-
if strings.TrimSpace(subject) == "" {
652+
if subject == "" {
650653
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`subject`"), nil, envelope.InputError)
651654
}
652-
if strings.TrimSpace(content) == "" {
655+
if content == "" {
653656
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`content`"), nil, envelope.InputError)
654657
}
655-
if strings.TrimSpace(email) == "" {
658+
if email == "" {
656659
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`contact_email`"), nil, envelope.InputError)
657660
}
658661
if firstName == "" {
659662
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`first_name`"), nil, envelope.InputError)
660663
}
664+
if !stringutil.ValidEmail(email) {
665+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil, envelope.InputError)
666+
}
661667

662668
user, err := app.user.GetAgent(auser.ID, "")
663669
if err != nil {
@@ -690,8 +696,8 @@ func handleCreateConversation(r *fastglue.Request) error {
690696
contact.ID,
691697
contact.ContactChannelID,
692698
inboxID,
693-
"", /** last_message **/
694-
time.Now(),
699+
"", /** last_message **/
700+
time.Now(), /** last_message_at **/
695701
subject,
696702
true, /** append reference number to subject **/
697703
)
@@ -701,8 +707,8 @@ func handleCreateConversation(r *fastglue.Request) error {
701707
}
702708

703709
// Send reply to the created conversation.
704-
if err := app.conversation.SendReply(nil /**media**/, inboxID, auser.ID, conversationUUID, content, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
705-
// 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.
706712
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
707713
app.lo.Error("error deleting conversation", "error", err)
708714
}

cmd/messages.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type messageReq struct {
1515
Attachments []int `json:"attachments"`
1616
Message string `json:"message"`
1717
Private bool `json:"private"`
18+
To []string `json:"to"`
1819
CC []string `json:"cc"`
1920
BCC []string `json:"bcc"`
2021
}
@@ -140,7 +141,7 @@ func handleSendMessage(r *fastglue.Request) error {
140141
return sendErrorEnvelope(r, err)
141142
}
142143

143-
// Check permission
144+
// Check access to conversation.
144145
conv, err := enforceConversationAccess(app, cuuid, user)
145146
if err != nil {
146147
return sendErrorEnvelope(r, err)
@@ -151,6 +152,7 @@ func handleSendMessage(r *fastglue.Request) error {
151152
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
152153
}
153154

155+
// Prepare attachments.
154156
for _, id := range req.Attachments {
155157
m, err := app.media.Get(id, "")
156158
if err != nil {
@@ -165,7 +167,7 @@ func handleSendMessage(r *fastglue.Request) error {
165167
return sendErrorEnvelope(r, err)
166168
}
167169
} else {
168-
if err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.CC, req.BCC, map[string]any{} /**meta**/); err != nil {
170+
if err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/); err != nil {
169171
return sendErrorEnvelope(r, err)
170172
}
171173
// Evaluate automation rules.

frontend/src/assets/styles/main.scss

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,7 @@
183183
}
184184

185185
.message-bubble {
186-
@apply flex
187-
flex-col
188-
px-4
189-
pt-2
190-
pb-3
191-
min-w-[30%] max-w-[70%]
192-
border
193-
overflow-x-auto
194-
rounded-xl;
186+
@apply flex flex-col px-4 pt-2 pb-3 w-fit min-w-[30%] max-w-full border overflow-x-auto rounded-xl;
195187
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
196188
table {
197189
width: 100% !important;

frontend/src/features/admin/notification/NotificationSettingForm.vue

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@
9797
</FormItem>
9898
</FormField>
9999

100+
<!-- Max Message Retries Field -->
101+
<FormField v-slot="{ componentField }" name="max_msg_retries">
102+
<FormItem>
103+
<FormLabel>{{ $t('admin.inbox.maxRetries') }}</FormLabel>
104+
<FormControl>
105+
<Input type="number" placeholder="3" v-bind="componentField" />
106+
</FormControl>
107+
<FormMessage />
108+
<FormDescription> {{ $t('admin.inbox.maxRetries.description') }} </FormDescription>
109+
</FormItem>
110+
</FormField>
111+
100112
<!-- Authentication Protocol Field -->
101113
<FormField v-slot="{ componentField }" name="auth_protocol">
102114
<FormItem>
@@ -136,18 +148,6 @@
136148
</FormItem>
137149
</FormField>
138150

139-
<!-- Max Message Retries Field -->
140-
<FormField v-slot="{ componentField }" name="max_msg_retries">
141-
<FormItem>
142-
<FormLabel>{{ $t('admin.inbox.maxRetries') }}</FormLabel>
143-
<FormControl>
144-
<Input type="number" placeholder="3" v-bind="componentField" />
145-
</FormControl>
146-
<FormMessage />
147-
<FormDescription> {{ $t('admin.inbox.maxRetries.description') }} </FormDescription>
148-
</FormItem>
149-
</FormField>
150-
151151
<!-- HELO Hostname Field -->
152152
<FormField v-slot="{ componentField }" name="hello_hostname">
153153
<FormItem>

frontend/src/features/conversation/ReplyBox.vue

Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -53,30 +53,20 @@
5353
:isSending="isSending"
5454
:uploadingFiles="uploadingFiles"
5555
:clearEditorContent="clearEditorContent"
56-
:htmlContent="htmlContent"
57-
:textContent="textContent"
58-
:selectedText="selectedText"
59-
:isBold="isBold"
60-
:isItalic="isItalic"
61-
:cursorPosition="cursorPosition"
6256
:contentToSet="contentToSet"
63-
:cc="cc"
64-
:bcc="bcc"
65-
:emailErrors="emailErrors"
66-
:messageType="messageType"
67-
:showBcc="showBcc"
68-
@update:htmlContent="htmlContent = $event"
69-
@update:textContent="textContent = $event"
70-
@update:selectedText="selectedText = $event"
71-
@update:isBold="isBold = $event"
72-
@update:isItalic="isItalic = $event"
73-
@update:cursorPosition="cursorPosition = $event"
74-
@toggleFullscreen="isEditorFullscreen = false"
75-
@update:messageType="messageType = $event"
76-
@update:cc="cc = $event"
77-
@update:bcc="bcc = $event"
78-
@update:showBcc="showBcc = $event"
79-
@updateEmailErrors="emailErrors = $event"
57+
v-model:htmlContent="htmlContent"
58+
v-model:textContent="textContent"
59+
v-model:selectedText="selectedText"
60+
v-model:isBold="isBold"
61+
v-model:isItalic="isItalic"
62+
v-model:cursorPosition="cursorPosition"
63+
v-model:to="to"
64+
v-model:cc="cc"
65+
v-model:bcc="bcc"
66+
v-model:emailErrors="emailErrors"
67+
v-model:messageType="messageType"
68+
v-model:showBcc="showBcc"
69+
@toggleFullscreen="isEditorFullscreen = true"
8070
@send="processSend"
8171
@fileUpload="handleFileUpload"
8272
@inlineImageUpload="handleInlineImageUpload"
@@ -99,30 +89,20 @@
9989
:isSending="isSending"
10090
:uploadingFiles="uploadingFiles"
10191
:clearEditorContent="clearEditorContent"
102-
:htmlContent="htmlContent"
103-
:textContent="textContent"
104-
:selectedText="selectedText"
105-
:isBold="isBold"
106-
:isItalic="isItalic"
107-
:cursorPosition="cursorPosition"
10892
:contentToSet="contentToSet"
109-
:cc="cc"
110-
:bcc="bcc"
111-
:emailErrors="emailErrors"
112-
:messageType="messageType"
113-
:showBcc="showBcc"
114-
@update:htmlContent="htmlContent = $event"
115-
@update:textContent="textContent = $event"
116-
@update:selectedText="selectedText = $event"
117-
@update:isBold="isBold = $event"
118-
@update:isItalic="isItalic = $event"
119-
@update:cursorPosition="cursorPosition = $event"
93+
v-model:htmlContent="htmlContent"
94+
v-model:textContent="textContent"
95+
v-model:selectedText="selectedText"
96+
v-model:isBold="isBold"
97+
v-model:isItalic="isItalic"
98+
v-model:cursorPosition="cursorPosition"
99+
v-model:to="to"
100+
v-model:cc="cc"
101+
v-model:bcc="bcc"
102+
v-model:emailErrors="emailErrors"
103+
v-model:messageType="messageType"
104+
v-model:showBcc="showBcc"
120105
@toggleFullscreen="isEditorFullscreen = true"
121-
@update:messageType="messageType = $event"
122-
@update:cc="cc = $event"
123-
@update:bcc="bcc = $event"
124-
@update:showBcc="showBcc = $event"
125-
@updateEmailErrors="emailErrors = $event"
126106
@send="processSend"
127107
@fileUpload="handleFileUpload"
128108
@inlineImageUpload="handleInlineImageUpload"
@@ -183,6 +163,7 @@ const clearEditorContent = ref(false)
183163
const isEditorFullscreen = ref(false)
184164
const isSending = ref(false)
185165
const messageType = ref('reply')
166+
const to = ref('')
186167
const cc = ref('')
187168
const bcc = ref('')
188169
const showBcc = ref(false)
@@ -353,6 +334,7 @@ const processSend = async () => {
353334
.map((img) => img.getAttribute('title'))
354335
.filter(Boolean)
355336
337+
// TODO: Inline images are not supported yet, this is some old boilerplate code.
356338
conversationStore.conversation.mediaFiles = conversationStore.conversation.mediaFiles.filter(
357339
(file) =>
358340
// Keep if:
@@ -375,12 +357,18 @@ const processSend = async () => {
375357
.split(',')
376358
.map((email) => email.trim())
377359
.filter((email) => email)
360+
: [],
361+
to: to.value
362+
? to.value
363+
.split(',')
364+
.map((email) => email.trim())
365+
.filter((email) => email)
378366
: []
379367
})
380368
}
381369
382370
// Apply macro actions if any.
383-
// For macros errors just show toast and clear the editor, as most likely it's the permission error.
371+
// For macro errors just show toast and clear the editor.
384372
if (conversationStore.conversation?.macro?.actions?.length > 0) {
385373
try {
386374
await api.applyMacro(
@@ -390,7 +378,6 @@ const processSend = async () => {
390378
)
391379
} catch (error) {
392380
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
393-
title: 'Error',
394381
variant: 'destructive',
395382
description: handleHTTPError(error).message
396383
})
@@ -453,7 +440,7 @@ watch(
453440
{ deep: true }
454441
)
455442
456-
// Initialize cc and bcc from conversation store
443+
// Initialize to, cc, and bcc fields with the current conversation's values.
457444
watch(
458445
() => conversationStore.currentCC,
459446
(newVal) => {
@@ -462,6 +449,14 @@ watch(
462449
{ deep: true, immediate: true }
463450
)
464451
452+
watch(
453+
() => conversationStore.currentTo,
454+
(newVal) => {
455+
to.value = newVal?.join(', ') || ''
456+
},
457+
{ immediate: true }
458+
)
459+
465460
watch(
466461
() => conversationStore.currentBCC,
467462
(newVal) => {

frontend/src/features/conversation/ReplyBoxContent.vue

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,21 @@
3737
</span>
3838
</div>
3939

40-
<!-- CC and BCC fields -->
40+
<!-- To, CC, and BCC fields -->
4141
<div
4242
:class="['space-y-3', isFullscreen ? 'p-4 border-b border-border' : 'mb-4']"
4343
v-if="messageType === 'reply'"
4444
>
45+
<div class="flex items-center space-x-2">
46+
<label class="w-12 text-sm font-medium text-muted-foreground">To:</label>
47+
<Input
48+
type="text"
49+
:placeholder="t('replyBox.emailAddresess')"
50+
v-model="to"
51+
class="flex-grow px-3 py-2 text-sm border rounded-md focus:ring-2 focus:ring-ring"
52+
@blur="validateEmails('to')"
53+
/>
54+
</div>
4555
<div class="flex items-center space-x-2">
4656
<label class="w-12 text-sm font-medium text-muted-foreground">CC:</label>
4757
<Input
@@ -71,7 +81,7 @@
7181
</div>
7282
</div>
7383

74-
<!-- CC and BCC field validation errors -->
84+
<!-- email errors -->
7585
<div
7686
v-if="emailErrors.length > 0"
7787
class="mb-4 px-2 py-1 bg-destructive/10 border border-destructive text-destructive rounded"
@@ -148,9 +158,11 @@ import AttachmentsPreview from '@/features/conversation/message/attachment/Attac
148158
import MacroActionsPreview from '@/features/conversation/MacroActionsPreview.vue'
149159
import ReplyBoxMenuBar from '@/features/conversation/ReplyBoxMenuBar.vue'
150160
import { useI18n } from 'vue-i18n'
161+
import { validateEmail } from '@/utils/strings'
151162
152163
// Define models for two-way binding
153164
const messageType = defineModel('messageType', { default: 'reply' })
165+
const to = defineModel('to', { default: '' })
154166
const cc = defineModel('cc', { default: '' })
155167
const bcc = defineModel('bcc', { default: '' })
156168
const showBcc = defineModel('showBcc', { default: false })
@@ -243,35 +255,35 @@ const enableSend = computed(() => {
243255
})
244256
245257
/**
246-
* Validate email addresses in the CC and BCC fields
247-
* @param {string} field - 'cc' or 'bcc'
258+
* Validate email addresses in the To, CC, and BCC fields
259+
* @param {string} field - 'to', 'cc', or 'bcc'
248260
*/
249261
const validateEmails = (field) => {
250-
const emails = field === 'cc' ? cc.value : bcc.value
262+
const emails = field === 'to' ? to.value : field === 'cc' ? cc.value : bcc.value
251263
const emailList = emails
252264
.split(',')
253265
.map((e) => e.trim())
254266
.filter((e) => e !== '')
255-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
256-
const invalidEmails = emailList.filter((email) => !emailRegex.test(email))
257267
258-
// Remove any existing errors for this field
259-
emailErrors.value = emailErrors.value.filter(
260-
(error) => !error.startsWith(`${t('replyBox.invalidEmailsIn')} ${field.toUpperCase()}`)
261-
)
268+
const invalidEmails = emailList.filter((email) => !validateEmail(email))
269+
270+
// Clear existing errors
271+
emailErrors.value = []
262272
263273
// Add new error if there are invalid emails
264274
if (invalidEmails.length > 0) {
265-
emailErrors.value.push(
266-
`${t('replyBox.invalidEmailsIn')} ${field.toUpperCase()}: ${invalidEmails.join(', ')}`
267-
)
275+
emailErrors.value = [
276+
...emailErrors.value,
277+
`${t('replyBox.invalidEmailsIn')} '${field}': ${invalidEmails.join(', ')}`
278+
]
268279
}
269280
}
270281
271282
/**
272283
* Send the reply or private note
273284
*/
274285
const handleSend = async () => {
286+
validateEmails('to')
275287
validateEmails('cc')
276288
validateEmails('bcc')
277289
if (emailErrors.value.length > 0) {

0 commit comments

Comments
 (0)