feat: Support cc and bcc in email replies (#3098)
Co-authored-by: Tejaswini <tejaswini@chatwoot.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
0e0632be22
commit
68e697c379
8 changed files with 135 additions and 25 deletions
|
@ -16,6 +16,7 @@ class Messages::MessageBuilder
|
|||
def perform
|
||||
@message = @conversation.messages.build(message_params)
|
||||
process_attachments
|
||||
process_emails
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
|
@ -34,6 +35,16 @@ class Messages::MessageBuilder
|
|||
end
|
||||
end
|
||||
|
||||
def process_emails
|
||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||
|
||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
|
||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
|
||||
|
||||
@message.content_attributes[:cc_emails] = cc_emails
|
||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||
end
|
||||
|
||||
def message_type
|
||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||
|
|
|
@ -8,6 +8,8 @@ export const buildCreatePayload = ({
|
|||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
|
@ -18,12 +20,16 @@ export const buildCreatePayload = ({
|
|||
}
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
echo_id: echoId,
|
||||
content_attributes: contentAttributes,
|
||||
cc_emails: ccEmails,
|
||||
bcc_emails: bccEmails,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
|
@ -41,6 +47,8 @@ class MessageApi extends ApiClient {
|
|||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}) {
|
||||
return axios({
|
||||
method: 'post',
|
||||
|
@ -51,6 +59,8 @@ class MessageApi extends ApiClient {
|
|||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
<div :class="wrapClass">
|
||||
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<bubble-mail-head
|
||||
v-if="isEmailContentType"
|
||||
:email-attributes="contentAttributes.email"
|
||||
:cc="emailHeadAttributes.cc"
|
||||
:bcc="emailHeadAttributes.bcc"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<bubble-text
|
||||
|
@ -222,6 +223,13 @@ export default {
|
|||
isIncoming() {
|
||||
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
||||
},
|
||||
emailHeadAttributes() {
|
||||
return {
|
||||
email: this.contentAttributes.email,
|
||||
cc: this.contentAttributes.cc_emails,
|
||||
bcc: this.contentAttributes.bcc_emails
|
||||
}
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
v-on-clickaway="hideEmojiPicker"
|
||||
:on-click="emojiOnClick"
|
||||
/>
|
||||
<reply-email-head
|
||||
v-if="showReplyHead"
|
||||
@set-emails="setCcEmails"
|
||||
:clear-mails="clearMails"
|
||||
/>
|
||||
<resizable-text-area
|
||||
v-if="!showRichContentEditor"
|
||||
ref="messageInput"
|
||||
|
@ -82,6 +87,7 @@ import CannedResponse from './CannedResponse';
|
|||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
|
||||
import ReplyEmailHead from './ReplyEmailHead';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
|
@ -104,6 +110,7 @@ export default {
|
|||
ResizableTextArea,
|
||||
AttachmentPreview,
|
||||
ReplyTopPanel,
|
||||
ReplyEmailHead,
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
},
|
||||
|
@ -134,6 +141,7 @@ export default {
|
|||
mentionSearchKey: '',
|
||||
hasUserMention: false,
|
||||
hasSlashCommand: false,
|
||||
clearMails: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -270,6 +278,9 @@ export default {
|
|||
}
|
||||
return !this.message.trim().replace(/\n/g, '').length;
|
||||
},
|
||||
showReplyHead(){
|
||||
return this.isAnEmailChannel;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
|
@ -354,6 +365,7 @@ export default {
|
|||
this.showAlert(errorMessage);
|
||||
}
|
||||
this.hideEmojiPicker();
|
||||
this.clearMails = false;
|
||||
}
|
||||
},
|
||||
replaceText(message) {
|
||||
|
@ -376,6 +388,7 @@ export default {
|
|||
clearMessage() {
|
||||
this.message = '';
|
||||
this.attachedFiles = [];
|
||||
this.clearMails = true;
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
|
@ -452,11 +465,23 @@ export default {
|
|||
messagePayload.file = attachment.resource.file;
|
||||
}
|
||||
|
||||
if(this.ccEmails) {
|
||||
messagePayload.ccEmails = this.ccEmails;
|
||||
}
|
||||
|
||||
if(this.bccEmails) {
|
||||
messagePayload.bccEmails = this.bccEmails;
|
||||
}
|
||||
|
||||
return messagePayload;
|
||||
},
|
||||
setFormatMode(value) {
|
||||
this.updateUISettings({ display_rich_content_editor: value });
|
||||
},
|
||||
setCcEmails(value) {
|
||||
this.bccEmails = value.bccEmails;
|
||||
this.ccEmails = value.ccEmails
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="input-group-wrap">
|
||||
<div class="input-group small" :class="{ error: $v.ccEmails.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.ccEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="ccEmails"
|
||||
v-model.trim="$v.ccEmailsVal.$model"
|
||||
type="email"
|
||||
:class="{ error: $v.ccEmails.$error }"
|
||||
:class="{ error: $v.ccEmailsVal.$error }"
|
||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||
@blur="$v.ccEmails.$touch"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
<woot-button
|
||||
|
@ -23,28 +23,28 @@
|
|||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<span v-if="$v.ccEmails.$error" class="message">
|
||||
<span v-if="$v.ccEmailsVal.$error" class="message">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="showBcc" class="input-group-wrap">
|
||||
<div class="input-group small" :class="{ error: $v.bccEmails.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.bccEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="bccEmails"
|
||||
v-model.trim="$v.bccEmailsVal.$model"
|
||||
type="email"
|
||||
:class="{ error: $v.bccEmails.$error }"
|
||||
:class="{ error: $v.bccEmailsVal.$error }"
|
||||
:placeholder="
|
||||
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||
"
|
||||
@blur="$v.bccEmails.$touch"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="$v.bccEmails.$error" class="message">
|
||||
<span v-if="$v.bccEmailsVal.$error" class="message">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -55,27 +55,25 @@
|
|||
import { validEmailsByComma } from './helpers/emailHeadHelper';
|
||||
export default {
|
||||
props: {
|
||||
ccEmails: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bccEmails: {
|
||||
type: String,
|
||||
default: '',
|
||||
clearMails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBcc: false,
|
||||
ccEmailsVal: '',
|
||||
bccEmailsVal: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
ccEmails: {
|
||||
ccEmailsVal: {
|
||||
hasValidEmails(value) {
|
||||
return validEmailsByComma(value);
|
||||
},
|
||||
},
|
||||
bccEmails: {
|
||||
bccEmailsVal: {
|
||||
hasValidEmails(value) {
|
||||
return validEmailsByComma(value);
|
||||
},
|
||||
|
@ -85,7 +83,20 @@ export default {
|
|||
handleAddBcc() {
|
||||
this.showBcc = true;
|
||||
},
|
||||
onBlur() {
|
||||
this.$v.$touch();
|
||||
this.$emit("set-emails", { bccEmails: this.bccEmailsVal, ccEmails: this.ccEmailsVal });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clearMails: function(value){
|
||||
if(value) {
|
||||
this.ccEmailsVal = '';
|
||||
this.bccEmailsVal = '';
|
||||
this.clearMails = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -36,6 +36,14 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
cc: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
bcc: {
|
||||
type: Array,
|
||||
default: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
toMails() {
|
||||
|
@ -43,11 +51,11 @@ export default {
|
|||
return to.join(', ');
|
||||
},
|
||||
ccMails() {
|
||||
const cc = this.emailAttributes.cc || [];
|
||||
const cc = this.emailAttributes.cc || this.cc || [];
|
||||
return cc.join(', ');
|
||||
},
|
||||
bccMails() {
|
||||
const bcc = this.emailAttributes.bcc || [];
|
||||
const bcc = this.emailAttributes.bcc || this.bcc || [];
|
||||
return bcc.join(', ');
|
||||
},
|
||||
subject() {
|
||||
|
|
|
@ -19,7 +19,9 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email
|
||||
in_reply_to: in_reply_to_email,
|
||||
cc: cc_bcc_emails[0],
|
||||
bcc: cc_bcc_emails[1]
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -39,7 +41,9 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email
|
||||
in_reply_to: in_reply_to_email,
|
||||
cc: cc_bcc_emails[0],
|
||||
bcc: cc_bcc_emails[1]
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -142,6 +146,15 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
nil
|
||||
end
|
||||
|
||||
def cc_bcc_emails
|
||||
content_attributes = @conversation.messages.outgoing.last&.content_attributes
|
||||
|
||||
return [] unless content_attributes
|
||||
return [] unless content_attributes[:cc_emails] || content_attributes[:bcc_emails]
|
||||
|
||||
[content_attributes[:cc_emails], content_attributes[:bcc_emails]]
|
||||
end
|
||||
|
||||
def inbound_email_enabled?
|
||||
@inbound_email_enabled ||= @account.feature_enabled?('inbound_emails') && @account.inbound_email_domain
|
||||
.present? && @account.support_email.present?
|
||||
|
|
|
@ -15,9 +15,28 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
|||
|
||||
context 'with summary' do
|
||||
let(:conversation) { create(:conversation, account: account, assignee: agent) }
|
||||
let(:message) { create(:message, account: account, conversation: conversation) }
|
||||
let(:message) do
|
||||
create(:message,
|
||||
account: account,
|
||||
conversation: conversation,
|
||||
content_attributes: {
|
||||
cc_emails: 'agent_cc1@example.com',
|
||||
bcc_emails: 'agent_bcc1@example.com'
|
||||
})
|
||||
end
|
||||
let(:cc_message) do
|
||||
create(:message,
|
||||
account: account,
|
||||
message_type: :outgoing,
|
||||
conversation: conversation,
|
||||
content_attributes: {
|
||||
cc_emails: 'agent_cc1@example.com',
|
||||
bcc_emails: 'agent_bcc1@example.com'
|
||||
})
|
||||
end
|
||||
let(:private_message) { create(:message, account: account, content: 'This is a private message', conversation: conversation) }
|
||||
let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now }
|
||||
let(:cc_mail) { described_class.reply_with_summary(cc_message.conversation, Time.zone.now).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("[##{message.conversation.display_id}] New messages on this conversation")
|
||||
|
@ -37,6 +56,11 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
|||
conversation.update(contact_last_seen_at: Time.zone.now)
|
||||
expect(mail).to eq nil
|
||||
end
|
||||
|
||||
it 'will send email to cc and bcc email addresses' do
|
||||
expect(cc_mail.cc.first).to eq(cc_message.content_attributes[:cc_emails])
|
||||
expect(cc_mail.bcc.first).to eq(cc_message.content_attributes[:bcc_emails])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without assignee' do
|
||||
|
|
Loading…
Reference in a new issue