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:
Nithin David Thomas 2021-10-11 13:00:48 +05:30 committed by GitHub
parent 0e0632be22
commit 68e697c379
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 25 deletions

View file

@ -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'

View file

@ -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,
}),
});
}

View file

@ -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);
},

View file

@ -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>

View file

@ -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>

View file

@ -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() {

View file

@ -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?

View file

@ -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