feat: IMAP Email Channel (#3298)
This change allows the user to configure both IMAP and SMTP for an email inbox. IMAP enables the user to see emails in Chatwoot. And user can use SMTP to reply to an email conversation. Users can use the default settings to send and receive emails for email inboxes if both IMAP and SMTP are disabled. Fixes #2520
This commit is contained in:
parent
8384d0b38e
commit
24e6a92297
25 changed files with 1040 additions and 57 deletions
|
@ -1,4 +1,5 @@
|
|||
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
include Api::V1::InboxesHelper
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
# we are already handling the authorization in fetch inbox
|
||||
|
@ -41,12 +42,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
def update
|
||||
@inbox.update(permitted_params.except(:channel))
|
||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||
|
||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||
|
||||
# Inbox update doesn't necessarily need channel attributes
|
||||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
|
33
app/helpers/api/v1/inboxes_helper.rb
Normal file
33
app/helpers/api/v1/inboxes_helper.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Api::V1::InboxesHelper
|
||||
def validate_email_channel(attributes)
|
||||
channel_data = permitted_params(attributes)[:channel]
|
||||
|
||||
validate_imap(channel_data)
|
||||
validate_smtp(channel_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_imap(channel_data)
|
||||
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
|
||||
|
||||
Mail.defaults do
|
||||
retriever_method :imap, { address: channel_data[:imap_address],
|
||||
port: channel_data[:imap_port],
|
||||
user_name: channel_data[:imap_email],
|
||||
password: channel_data[:imap_password],
|
||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||
end
|
||||
|
||||
Mail.connection do # rubocop:disable:block
|
||||
end
|
||||
end
|
||||
|
||||
def validate_smtp(channel_data)
|
||||
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
|
||||
|
||||
smtp = Net::SMTP.start(channel_data[:smtp_address], channel_data[:smtp_port], channel_data[:smtp_domain], channel_data[:smtp_email],
|
||||
channel_data[:smtp_password], :login)
|
||||
smtp.finish unless smtp&.nil?
|
||||
end
|
||||
end
|
|
@ -401,6 +401,65 @@
|
|||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
||||
"CHOOSE": "Choose"
|
||||
}
|
||||
},
|
||||
"IMAP": {
|
||||
"TITLE": "IMAP",
|
||||
"SUBTITLE": "Set your IMAP details",
|
||||
"UPDATE": "Update IMAP settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
|
||||
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
|
||||
"EDIT": {
|
||||
"SUCCESS_MESSAGE": "IMAP settings updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update IMAP settings"
|
||||
},
|
||||
"ADDRESS": {
|
||||
"LABEL": "Address",
|
||||
"PLACE_HOLDER": "Address (Eg: imap.gmail.com)"
|
||||
},
|
||||
"PORT": {
|
||||
"LABEL": "Port",
|
||||
"PLACE_HOLDER": "Port"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email",
|
||||
"PLACE_HOLDER": "Email"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
"PLACE_HOLDER": "Password"
|
||||
},
|
||||
"ENABLE_SSL": "Enable SSL"
|
||||
},
|
||||
"SMTP": {
|
||||
"TITLE": "SMTP",
|
||||
"SUBTITLE": "Set your SMTP details",
|
||||
"UPDATE": "Update SMTP settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable SMTP configuration for this inbox",
|
||||
"TOGGLE_HELP": "Enabling SMTP will help the user to send email",
|
||||
"EDIT": {
|
||||
"SUCCESS_MESSAGE": "SMTP settings updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update SMTP settings"
|
||||
},
|
||||
"ADDRESS": {
|
||||
"LABEL": "Address",
|
||||
"PLACE_HOLDER": "Address (Eg: smtp.gmail.com)"
|
||||
},
|
||||
"PORT": {
|
||||
"LABEL": "Port",
|
||||
"PLACE_HOLDER": "Port"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email",
|
||||
"PLACE_HOLDER": "Email"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
"PLACE_HOLDER": "Password"
|
||||
},
|
||||
"DOMAIN": {
|
||||
"LABEL": "Domain",
|
||||
"PLACE_HOLDER": "Domain"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<div class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.IMAP.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.IMAP.SUBTITLE')"
|
||||
>
|
||||
<form @submit.prevent="updateInbox">
|
||||
<label for="toggle-imap-enable">
|
||||
<input
|
||||
v-model="isIMAPEnabled"
|
||||
type="checkbox"
|
||||
name="toggle-imap-enable"
|
||||
/>
|
||||
{{ $t('INBOX_MGMT.IMAP.TOGGLE_AVAILABILITY') }}
|
||||
</label>
|
||||
<p>{{ $t('INBOX_MGMT.IMAP.TOGGLE_HELP') }}</p>
|
||||
<div v-if="isIMAPEnabled" class="imap-details-wrap">
|
||||
<woot-input
|
||||
v-model.trim="address"
|
||||
:class="{ error: $v.address.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.IMAP.ADDRESS.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.IMAP.ADDRESS.PLACE_HOLDER')"
|
||||
@blur="$v.address.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="port"
|
||||
type="number"
|
||||
:class="{ error: $v.port.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.IMAP.PORT.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.IMAP.PORT.PLACE_HOLDER')"
|
||||
@blur="$v.port.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="email"
|
||||
:class="{ error: $v.email.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.IMAP.EMAIL.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.IMAP.EMAIL.PLACE_HOLDER')"
|
||||
@blur="$v.email.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="password"
|
||||
:class="{ error: $v.password.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.IMAP.PASSWORD.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.IMAP.PASSWORD.PLACE_HOLDER')"
|
||||
type="password"
|
||||
@blur="$v.password.$touch"
|
||||
/>
|
||||
<label for="toggle-enable-ssl">
|
||||
<input
|
||||
v-model="isSSLEnabled"
|
||||
type="checkbox"
|
||||
name="toggle-enable-ssl"
|
||||
/>
|
||||
{{ $t('INBOX_MGMT.IMAP.ENABLE_SSL') }}
|
||||
</label>
|
||||
</div>
|
||||
<woot-submit-button
|
||||
:button-text="$t('INBOX_MGMT.IMAP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
:disabled="($v.$invalid && isIMAPEnabled) || uiFlags.isUpdatingIMAP"
|
||||
/>
|
||||
</form>
|
||||
</settings-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingsSection from 'dashboard/components/SettingsSection';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isIMAPEnabled: false,
|
||||
address: '',
|
||||
port: '',
|
||||
email: '',
|
||||
password: '',
|
||||
isSSLEnabled: true,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
address: { required },
|
||||
port: { required, minLength: minLength(2) },
|
||||
email: { required, email },
|
||||
password: { required },
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
this.setDefaults();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setDefaults();
|
||||
},
|
||||
methods: {
|
||||
setDefaults() {
|
||||
const {
|
||||
imap_enabled,
|
||||
imap_address,
|
||||
imap_port,
|
||||
imap_email,
|
||||
imap_password,
|
||||
imap_enable_ssl,
|
||||
} = this.inbox;
|
||||
this.isIMAPEnabled = imap_enabled;
|
||||
this.address = imap_address;
|
||||
this.port = imap_port;
|
||||
this.email = imap_email;
|
||||
this.password = imap_password;
|
||||
this.isSSLEnabled = imap_enable_ssl;
|
||||
},
|
||||
async updateInbox() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const payload = {
|
||||
id: this.inbox.id,
|
||||
formData: false,
|
||||
channel: {
|
||||
imap_enabled: this.isIMAPEnabled,
|
||||
imap_address: this.address,
|
||||
imap_port: this.port,
|
||||
imap_email: this.email,
|
||||
imap_password: this.password,
|
||||
imap_enable_ssl: this.isSSLEnabled,
|
||||
imap_inbox_synced_at: this.isIMAPEnabled
|
||||
? new Date().toISOString()
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
await this.$store.dispatch('inboxes/updateInboxIMAP', payload);
|
||||
this.showAlert(this.$t('INBOX_MGMT.IMAP.EDIT.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.IMAP.EDIT.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.imap-details-wrap {
|
||||
margin-bottom: var(--space-medium);
|
||||
}
|
||||
</style>
|
|
@ -353,6 +353,8 @@
|
|||
<woot-code :script="inbox.forward_to_email"></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
<imap-settings :inbox="inbox" />
|
||||
<smtp-settings :inbox="inbox" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'preChatForm'">
|
||||
|
@ -378,6 +380,8 @@ import FacebookReauthorize from './facebook/Reauthorize';
|
|||
import PreChatFormSettings from './PreChatForm/Settings';
|
||||
import WeeklyAvailability from './components/WeeklyAvailability';
|
||||
import GreetingsEditor from 'shared/components/GreetingsEditor';
|
||||
import ImapSettings from './ImapSettings';
|
||||
import SmtpSettings from './SmtpSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -387,6 +391,8 @@ export default {
|
|||
PreChatFormSettings,
|
||||
WeeklyAvailability,
|
||||
GreetingsEditor,
|
||||
ImapSettings,
|
||||
SmtpSettings,
|
||||
},
|
||||
mixins: [alertMixin, configMixin, inboxMixin],
|
||||
data() {
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<div class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SMTP.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.SMTP.SUBTITLE')"
|
||||
>
|
||||
<form @submit.prevent="updateInbox">
|
||||
<label for="toggle-enable-smtp">
|
||||
<input
|
||||
v-model="isSMTPEnabled"
|
||||
type="checkbox"
|
||||
name="toggle-enable-smtp"
|
||||
/>
|
||||
{{ $t('INBOX_MGMT.SMTP.TOGGLE_AVAILABILITY') }}
|
||||
</label>
|
||||
<p>{{ $t('INBOX_MGMT.SMTP.TOGGLE_HELP') }}</p>
|
||||
<div v-if="isSMTPEnabled" class="smtp-details-wrap">
|
||||
<woot-input
|
||||
v-model.trim="address"
|
||||
:class="{ error: $v.address.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.ADDRESS.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.ADDRESS.PLACE_HOLDER')"
|
||||
@blur="$v.address.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="port"
|
||||
type="number"
|
||||
:class="{ error: $v.port.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.PORT.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.PORT.PLACE_HOLDER')"
|
||||
@blur="$v.port.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="email"
|
||||
:class="{ error: $v.email.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.EMAIL.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.EMAIL.PLACE_HOLDER')"
|
||||
@blur="$v.email.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="password"
|
||||
:class="{ error: $v.password.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.PASSWORD.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.PASSWORD.PLACE_HOLDER')"
|
||||
type="password"
|
||||
@blur="$v.password.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="domain"
|
||||
:class="{ error: $v.domain.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.DOMAIN.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.DOMAIN.PLACE_HOLDER')"
|
||||
@blur="$v.domain.$touch"
|
||||
/>
|
||||
</div>
|
||||
<woot-submit-button
|
||||
:button-text="$t('INBOX_MGMT.SMTP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
:disabled="($v.$invalid && isSMTPEnabled) || uiFlags.isUpdatingSMTP"
|
||||
/>
|
||||
</form>
|
||||
</settings-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingsSection from 'dashboard/components/SettingsSection';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSMTPEnabled: false,
|
||||
address: '',
|
||||
port: '',
|
||||
email: '',
|
||||
password: '',
|
||||
domain: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
address: { required },
|
||||
port: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
email: { required, email },
|
||||
password: { required },
|
||||
domain: { required },
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
this.setDefaults();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setDefaults();
|
||||
},
|
||||
methods: {
|
||||
setDefaults() {
|
||||
const {
|
||||
smtp_enabled,
|
||||
smtp_address,
|
||||
smtp_port,
|
||||
smtp_email,
|
||||
smtp_password,
|
||||
smtp_domain,
|
||||
} = this.inbox;
|
||||
this.isSMTPEnabled = smtp_enabled;
|
||||
this.address = smtp_address;
|
||||
this.port = smtp_port;
|
||||
this.email = smtp_email;
|
||||
this.password = smtp_password;
|
||||
this.domain = smtp_domain;
|
||||
},
|
||||
async updateInbox() {
|
||||
try {
|
||||
const payload = {
|
||||
id: this.inbox.id,
|
||||
formData: false,
|
||||
channel: {
|
||||
smtp_enabled: this.isSMTPEnabled,
|
||||
smtp_address: this.address,
|
||||
smtp_port: this.port,
|
||||
smtp_email: this.email,
|
||||
smtp_password: this.password,
|
||||
smtp_domain: this.domain,
|
||||
},
|
||||
};
|
||||
await this.$store.dispatch('inboxes/updateInboxSMTP', payload);
|
||||
this.showAlert(this.$t('INBOX_MGMT.SMTP.EDIT.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.SMTP.EDIT.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.smtp-details-wrap {
|
||||
margin-bottom: var(--space-medium);
|
||||
}
|
||||
</style>
|
|
@ -35,6 +35,8 @@ export const state = {
|
|||
isUpdating: false,
|
||||
isUpdatingAutoAssignment: false,
|
||||
isDeleting: false,
|
||||
isUpdatingIMAP: false,
|
||||
isUpdatingSMTP: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -164,6 +166,52 @@ export const actions = {
|
|||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
updateInboxIMAP: async (
|
||||
{ commit },
|
||||
{ id, formData = true, ...inboxParams }
|
||||
) => {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, {
|
||||
isUpdatingIMAP: true,
|
||||
});
|
||||
try {
|
||||
const response = await InboxesAPI.update(
|
||||
id,
|
||||
formData ? buildInboxData(inboxParams) : inboxParams
|
||||
);
|
||||
commit(types.default.EDIT_INBOXES, response.data);
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, {
|
||||
isUpdatingIMAP: false,
|
||||
});
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, {
|
||||
isUpdatingIMAP: false,
|
||||
});
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
updateInboxSMTP: async (
|
||||
{ commit },
|
||||
{ id, formData = true, ...inboxParams }
|
||||
) => {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, {
|
||||
isUpdatingSMTP: true,
|
||||
});
|
||||
try {
|
||||
const response = await InboxesAPI.update(
|
||||
id,
|
||||
formData ? buildInboxData(inboxParams) : inboxParams
|
||||
);
|
||||
commit(types.default.EDIT_INBOXES, response.data);
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, {
|
||||
isUpdatingSMTP: false,
|
||||
});
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, {
|
||||
isUpdatingSMTP: false,
|
||||
});
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, inboxId) => {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
|
|
|
@ -107,6 +107,66 @@ describe('#actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#updateInboxIMAP', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const updatedInbox = inboxList[0];
|
||||
|
||||
axios.patch.mockResolvedValue({ data: updatedInbox });
|
||||
await actions.updateInboxIMAP(
|
||||
{ commit },
|
||||
{ id: updatedInbox.id, inbox: { channel: { imap_enabled: true } } }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: true }],
|
||||
[types.default.EDIT_INBOXES, updatedInbox],
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.updateInboxIMAP(
|
||||
{ commit },
|
||||
{ id: inboxList[0].id, inbox: { channel: { imap_enabled: true } } }
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: true }],
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingIMAP: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateInboxSMTP', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const updatedInbox = inboxList[0];
|
||||
|
||||
axios.patch.mockResolvedValue({ data: updatedInbox });
|
||||
await actions.updateInboxSMTP(
|
||||
{ commit },
|
||||
{ id: updatedInbox.id, inbox: { channel: { smtp_enabled: true } } }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: true }],
|
||||
[types.default.EDIT_INBOXES, updatedInbox],
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.updateInboxSMTP(
|
||||
{ commit },
|
||||
{ id: inboxList[0].id, inbox: { channel: { smtp_enabled: true } } }
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: true }],
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isUpdatingSMTP: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({ data: inboxList[0] });
|
||||
|
|
9
app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
Normal file
9
app/jobs/inboxes/fetch_imap_email_inboxes_job.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform
|
||||
Inbox.where(channel_type: 'Channel::Email').all.each do |inbox|
|
||||
Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
|
||||
end
|
||||
end
|
||||
end
|
24
app/jobs/inboxes/fetch_imap_emails_job.rb
Normal file
24
app/jobs/inboxes/fetch_imap_emails_job.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
class Inboxes::FetchImapEmailsJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(channel)
|
||||
Mail.defaults do
|
||||
retriever_method :imap, address: channel.imap_address,
|
||||
port: channel.imap_port,
|
||||
user_name: channel.imap_email,
|
||||
password: channel.imap_password,
|
||||
enable_ssl: channel.imap_enable_ssl
|
||||
end
|
||||
|
||||
new_mails = false
|
||||
|
||||
Mail.find(what: :last, count: 10, order: :desc).each do |inbound_mail|
|
||||
if inbound_mail.date.utc >= channel.imap_inbox_synced_at
|
||||
Imap::ImapMailbox.new.process(inbound_mail, channel)
|
||||
new_mails = true
|
||||
end
|
||||
end
|
||||
|
||||
Channel::Email.update(channel.id, imap_inbox_synced_at: Time.now.utc) if new_mails
|
||||
end
|
||||
end
|
79
app/mailboxes/imap/imap_mailbox.rb
Normal file
79
app/mailboxes/imap/imap_mailbox.rb
Normal file
|
@ -0,0 +1,79 @@
|
|||
class Imap::ImapMailbox
|
||||
include MailboxHelper
|
||||
attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
|
||||
|
||||
def process(mail, channel)
|
||||
@inbound_mail = mail
|
||||
@channel = channel
|
||||
load_account
|
||||
load_inbox
|
||||
decorate_mail
|
||||
|
||||
# prevent loop from chatwoot notification emails
|
||||
return if notification_email_from_chatwoot?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
find_or_create_contact
|
||||
find_or_create_conversation
|
||||
create_message
|
||||
add_attachments_to_message
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account
|
||||
@account = @channel.account
|
||||
end
|
||||
|
||||
def load_inbox
|
||||
@inbox = @channel.inbox
|
||||
end
|
||||
|
||||
def decorate_mail
|
||||
@processed_mail = MailPresenter.new(@inbound_mail, @account)
|
||||
end
|
||||
|
||||
def find_conversation_by_in_reply_to
|
||||
return if in_reply_to.blank?
|
||||
|
||||
message = @inbox.messages.find_by(source_id: in_reply_to)
|
||||
if message.nil?
|
||||
@inbox.conversations.where("additional_attributes->>'in_reply_to' = ?", in_reply_to).first
|
||||
else
|
||||
@inbox.conversations.find(message.conversation_id)
|
||||
end
|
||||
end
|
||||
|
||||
def in_reply_to
|
||||
@inbound_mail.in_reply_to
|
||||
end
|
||||
|
||||
def find_or_create_conversation
|
||||
@conversation = find_conversation_by_in_reply_to || ::Conversation.create!({ account_id: @account.id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: {
|
||||
source: 'email',
|
||||
in_reply_to: in_reply_to,
|
||||
mail_subject: @processed_mail.subject,
|
||||
initiated_at: {
|
||||
timestamp: Time.now.utc
|
||||
}
|
||||
} })
|
||||
end
|
||||
|
||||
def find_or_create_contact
|
||||
@contact = @inbox.contacts.find_by(email: @processed_mail.original_sender)
|
||||
if @contact.present?
|
||||
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
|
||||
else
|
||||
create_contact
|
||||
end
|
||||
end
|
||||
|
||||
def identify_contact_name
|
||||
processed_mail.sender_name || processed_mail.from.first.split('@').first
|
||||
end
|
||||
end
|
|
@ -29,6 +29,21 @@ module MailboxHelper
|
|||
@message.save!
|
||||
end
|
||||
|
||||
def create_contact
|
||||
@contact_inbox = ::ContactBuilder.new(
|
||||
source_id: "email:#{processed_mail.message_id}",
|
||||
inbox: @inbox,
|
||||
contact_attributes: {
|
||||
name: identify_contact_name,
|
||||
email: processed_mail.original_sender,
|
||||
additional_attributes: {
|
||||
source_id: "email:#{processed_mail.message_id}"
|
||||
}
|
||||
}
|
||||
).perform
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def notification_email_from_chatwoot?
|
||||
# notification emails are send via mailer sender email address. so it should match
|
||||
@processed_mail.original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')).address
|
||||
|
|
|
@ -78,21 +78,6 @@ class SupportMailbox < ApplicationMailbox
|
|||
end
|
||||
end
|
||||
|
||||
def create_contact
|
||||
@contact_inbox = ::ContactBuilder.new(
|
||||
source_id: "email:#{processed_mail.message_id}",
|
||||
inbox: @inbox,
|
||||
contact_attributes: {
|
||||
name: identify_contact_name,
|
||||
email: @processed_mail.original_sender,
|
||||
additional_attributes: {
|
||||
source_id: "email:#{processed_mail.message_id}"
|
||||
}
|
||||
}
|
||||
).perform
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def identify_contact_name
|
||||
processed_mail.sender_name || processed_mail.from.first.split('@').first
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class ConversationReplyMailer < ApplicationMailer
|
||||
include ConversationReplyMailerHelper
|
||||
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
|
||||
layout :choose_layout
|
||||
|
||||
|
@ -12,16 +13,7 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
new_messages = @conversation.messages.chat.where('id >= ?', last_queued_id)
|
||||
@messages = recap_messages + new_messages
|
||||
@messages = @messages.select(&:email_reply_summarizable?)
|
||||
mail({
|
||||
to: @contact&.email,
|
||||
from: from_email_with_name,
|
||||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email,
|
||||
cc: cc_bcc_emails[0],
|
||||
bcc: cc_bcc_emails[1]
|
||||
})
|
||||
prepare_mail(true)
|
||||
end
|
||||
|
||||
def reply_without_summary(conversation, last_queued_id)
|
||||
|
@ -34,14 +26,7 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
@messages = @messages.reject { |m| m.template? && !m.input_csat? }
|
||||
return false if @messages.count.zero?
|
||||
|
||||
mail({
|
||||
to: @contact&.email,
|
||||
from: from_email_with_name,
|
||||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email
|
||||
})
|
||||
prepare_mail(false)
|
||||
end
|
||||
|
||||
def email_reply(message)
|
||||
|
@ -49,17 +34,7 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
|
||||
init_conversation_attributes(message.conversation)
|
||||
@message = message
|
||||
|
||||
reply_mail_object = mail({
|
||||
to: @contact&.email,
|
||||
from: from_email_with_name,
|
||||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email,
|
||||
cc: cc_bcc_emails[0],
|
||||
bcc: cc_bcc_emails[1]
|
||||
})
|
||||
reply_mail_object = prepare_mail(true)
|
||||
|
||||
message.update(source_id: reply_mail_object.message_id)
|
||||
end
|
||||
|
@ -86,6 +61,7 @@ class ConversationReplyMailer < ApplicationMailer
|
|||
@contact = @conversation.contact
|
||||
@agent = @conversation.assignee
|
||||
@inbox = @conversation.inbox
|
||||
@channel = @inbox.channel
|
||||
end
|
||||
|
||||
def should_use_conversation_email_address?
|
||||
|
|
51
app/mailers/conversation_reply_mailer_helper.rb
Normal file
51
app/mailers/conversation_reply_mailer_helper.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
module ConversationReplyMailerHelper
|
||||
def prepare_mail(cc_bcc_enabled)
|
||||
@options = {
|
||||
to: @contact&.email,
|
||||
from: email_from,
|
||||
reply_to: email_reply_to,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email
|
||||
}
|
||||
|
||||
if cc_bcc_enabled
|
||||
@options[:cc] = cc_bcc_emails[0]
|
||||
@options[:bcc] = cc_bcc_emails[1]
|
||||
end
|
||||
|
||||
set_delivery_method
|
||||
mail(@options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_delivery_method
|
||||
return unless @inbox.inbox_type == 'Email' && @channel.smtp_enabled
|
||||
|
||||
smtp_settings = {
|
||||
address: @channel.smtp_address,
|
||||
port: @channel.smtp_port,
|
||||
user_name: @channel.smtp_email,
|
||||
password: @channel.smtp_password,
|
||||
domain: @channel.smtp_domain,
|
||||
enable_starttls_auto: @channel.smtp_enable_starttls_auto,
|
||||
authentication: @channel.smtp_authentication
|
||||
}
|
||||
|
||||
@options[:delivery_method] = :smtp
|
||||
@options[:delivery_method_options] = smtp_settings
|
||||
end
|
||||
|
||||
def email_smtp_enabled
|
||||
@inbox.inbox_type == 'Email' && @channel.imap_enabled
|
||||
end
|
||||
|
||||
def email_from
|
||||
email_smtp_enabled ? @channel.smtp_email : from_email_with_name
|
||||
end
|
||||
|
||||
def email_reply_to
|
||||
email_smtp_enabled ? @channel.smtp_email : reply_email
|
||||
end
|
||||
end
|
|
@ -2,12 +2,27 @@
|
|||
#
|
||||
# Table name: channel_email
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# email :string not null
|
||||
# forward_to_email :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
# id :bigint not null, primary key
|
||||
# email :string not null
|
||||
# forward_to_email :string not null
|
||||
# imap_address :string default("")
|
||||
# imap_email :string default("")
|
||||
# imap_enable_ssl :boolean default(TRUE)
|
||||
# imap_enabled :boolean default(FALSE)
|
||||
# imap_inbox_synced_at :datetime
|
||||
# imap_password :string default("")
|
||||
# imap_port :integer default(0)
|
||||
# smtp_address :string default("")
|
||||
# smtp_authentication :string default("login")
|
||||
# smtp_domain :string default("")
|
||||
# smtp_email :string default("")
|
||||
# smtp_enable_starttls_auto :boolean default(TRUE)
|
||||
# smtp_enabled :boolean default(FALSE)
|
||||
# smtp_password :string default("")
|
||||
# smtp_port :integer default(0)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
@ -19,7 +34,8 @@ class Channel::Email < ApplicationRecord
|
|||
include Channelable
|
||||
|
||||
self.table_name = 'channel_email'
|
||||
EDITABLE_ATTRS = [:email].freeze
|
||||
EDITABLE_ATTRS = [:email, :imap_enabled, :imap_email, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_inbox_synced_at,
|
||||
:smtp_enabled, :smtp_email, :smtp_password, :smtp_address, :smtp_port, :smtp_domain].freeze
|
||||
|
||||
validates :email, uniqueness: true
|
||||
validates :forward_to_email, uniqueness: true
|
||||
|
|
|
@ -45,6 +45,22 @@ json.medium resource.channel.try(:medium) if resource.twilio?
|
|||
json.forward_to_email resource.channel.try(:forward_to_email)
|
||||
json.email resource.channel.try(:email) if resource.email?
|
||||
|
||||
## IMAP
|
||||
json.imap_email resource.channel.try(:imap_email) if resource.email?
|
||||
json.imap_password resource.channel.try(:imap_password) if resource.email?
|
||||
json.imap_address resource.channel.try(:imap_address) if resource.email?
|
||||
json.imap_port resource.channel.try(:imap_port) if resource.email?
|
||||
json.imap_enabled resource.channel.try(:imap_enabled) if resource.email?
|
||||
json.imap_enable_ssl resource.channel.try(:imap_enable_ssl) if resource.email?
|
||||
|
||||
## SMTP
|
||||
json.smtp_email resource.channel.try(:smtp_email) if resource.email?
|
||||
json.smtp_password resource.channel.try(:smtp_password) if resource.email?
|
||||
json.smtp_address resource.channel.try(:smtp_address) if resource.email?
|
||||
json.smtp_port resource.channel.try(:smtp_port) if resource.email?
|
||||
json.smtp_enabled resource.channel.try(:smtp_enabled) if resource.email?
|
||||
json.smtp_domain resource.channel.try(:smtp_domain) if resource.email?
|
||||
|
||||
## API Channel Attributes
|
||||
json.webhook_url resource.channel.try(:webhook_url) if resource.api?
|
||||
json.inbox_identifier resource.channel.try(:identifier) if resource.api?
|
||||
|
|
|
@ -3,13 +3,18 @@
|
|||
|
||||
# executed At 12:00 on every day-of-month.
|
||||
internal_check_new_versions_job:
|
||||
cron: "0 12 */1 * *"
|
||||
class: "Internal::CheckNewVersionsJob"
|
||||
cron: '0 12 */1 * *'
|
||||
class: 'Internal::CheckNewVersionsJob'
|
||||
queue: scheduled_jobs
|
||||
|
||||
# executed At every 5th minute..
|
||||
trigger_scheduled_items_job:
|
||||
cron: "*/5 * * * *"
|
||||
class: "TriggerScheduledItemsJob"
|
||||
cron: '*/5 * * * *'
|
||||
class: 'TriggerScheduledItemsJob'
|
||||
queue: scheduled_jobs
|
||||
|
||||
# executed At every minute.
|
||||
trigger_scheduled_items_job:
|
||||
cron: '*/1 * * * *'
|
||||
class: 'Inboxes::FetchImapEmailInboxesJob'
|
||||
queue: scheduled_jobs
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
class AddImapSmtpConfigToChannelEmail < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
change_table :channel_email, bulk: true do |t|
|
||||
# IMAP
|
||||
t.boolean :imap_enabled, default: false
|
||||
t.string :imap_address, default: ''
|
||||
t.integer :imap_port, default: 0
|
||||
t.string :imap_email, default: ''
|
||||
t.string :imap_password, default: ''
|
||||
t.boolean :imap_enable_ssl, default: true
|
||||
t.datetime :imap_inbox_synced_at
|
||||
|
||||
# SMTP
|
||||
t.boolean :smtp_enabled, default: false
|
||||
t.string :smtp_address, default: ''
|
||||
t.integer :smtp_port, default: 0
|
||||
t.string :smtp_email, default: ''
|
||||
t.string :smtp_password, default: ''
|
||||
t.string :smtp_domain, default: ''
|
||||
t.boolean :smtp_enable_starttls_auto, default: true
|
||||
t.string :smtp_authentication, default: 'login'
|
||||
end
|
||||
end
|
||||
end
|
15
db/schema.rb
15
db/schema.rb
|
@ -181,6 +181,21 @@ ActiveRecord::Schema.define(version: 2021_11_18_100301) do
|
|||
t.string "forward_to_email", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.boolean "imap_enabled", default: false
|
||||
t.string "imap_address", default: ""
|
||||
t.integer "imap_port", default: 0
|
||||
t.string "imap_email", default: ""
|
||||
t.string "imap_password", default: ""
|
||||
t.boolean "imap_enable_ssl", default: true
|
||||
t.datetime "imap_inbox_synced_at"
|
||||
t.boolean "smtp_enabled", default: false
|
||||
t.string "smtp_address", default: ""
|
||||
t.integer "smtp_port", default: 0
|
||||
t.string "smtp_email", default: ""
|
||||
t.string "smtp_password", default: ""
|
||||
t.string "smtp_domain", default: ""
|
||||
t.boolean "smtp_enable_starttls_auto", default: true
|
||||
t.string "smtp_authentication", default: "login"
|
||||
t.index ["email"], name: "index_channel_email_on_email", unique: true
|
||||
t.index ["forward_to_email"], name: "index_channel_email_on_forward_to_email", unique: true
|
||||
end
|
||||
|
|
|
@ -376,6 +376,57 @@ RSpec.describe 'Inboxes API', type: :request do
|
|||
expect(email_channel.reload.email).to eq('emailtest@email.test')
|
||||
end
|
||||
|
||||
it 'updates email inbox with imap when administrator' do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
imap_connection = double
|
||||
allow(Mail).to receive(:connection).and_return(imap_connection)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: {
|
||||
channel: {
|
||||
imap_enabled: true,
|
||||
imap_address: 'imap.gmail.com',
|
||||
imap_port: 993,
|
||||
imap_email: 'imaptest@gmail.com'
|
||||
}
|
||||
},
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(email_channel.reload.imap_enabled).to be true
|
||||
expect(email_channel.reload.imap_address).to eq('imap.gmail.com')
|
||||
expect(email_channel.reload.imap_port).to eq(993)
|
||||
end
|
||||
|
||||
it 'updates email inbox with smtp when administrator' do
|
||||
email_channel = create(:channel_email, account: account)
|
||||
email_inbox = create(:inbox, channel: email_channel, account: account)
|
||||
|
||||
smtp_connection = double
|
||||
allow(smtp_connection).to receive(:finish).and_return(true)
|
||||
allow(Net::SMTP).to receive(:start).and_return(smtp_connection)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: {
|
||||
channel: {
|
||||
smtp_enabled: true,
|
||||
smtp_address: 'smtp.gmail.com',
|
||||
smtp_port: 587,
|
||||
smtp_email: 'smtptest@gmail.com'
|
||||
}
|
||||
},
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(email_channel.reload.smtp_enabled).to be true
|
||||
expect(email_channel.reload.smtp_address).to eq('smtp.gmail.com')
|
||||
expect(email_channel.reload.smtp_port).to eq(587)
|
||||
end
|
||||
|
||||
it 'updates avatar when administrator' do
|
||||
# no avatar before upload
|
||||
expect(inbox.avatar.attached?).to eq(false)
|
||||
|
|
27
spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
Normal file
27
spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::FetchImapEmailInboxesJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:imap_email_channel) do
|
||||
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_email: 'imap@gmail.com',
|
||||
imap_password: 'password', account: account)
|
||||
end
|
||||
let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when called' do
|
||||
it 'fetch all the email channels' do
|
||||
imap_email_inboxes = double
|
||||
allow(imap_email_inboxes).to receive(:all).and_return([email_inbox])
|
||||
allow(Inbox).to receive(:where).and_return(imap_email_inboxes)
|
||||
|
||||
expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(imap_email_channel).once
|
||||
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
end
|
37
spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
Normal file
37
spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Inboxes::FetchImapEmailsJob, type: :job do
|
||||
let(:account) { create(:account) }
|
||||
let(:imap_email_channel) do
|
||||
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_email: 'imap@gmail.com',
|
||||
imap_password: 'password', imap_inbox_synced_at: Time.now.utc - 10, account: account)
|
||||
end
|
||||
let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when imap fetch latest 10 emails' do
|
||||
it 'check for the new emails' do
|
||||
mail_date = Time.now.utc
|
||||
mail = Mail.new do
|
||||
to 'test@outlook.com'
|
||||
from 'test@gmail.com'
|
||||
subject :test.to_s
|
||||
body 'hello'
|
||||
date mail_date
|
||||
end
|
||||
|
||||
allow(Mail).to receive(:find).and_return([mail])
|
||||
imap_mailbox = double
|
||||
allow(Imap::ImapMailbox).to receive(:new).and_return(imap_mailbox)
|
||||
expect(imap_mailbox).to receive(:process).with(mail, imap_email_channel).once
|
||||
|
||||
described_class.perform_now(imap_email_channel)
|
||||
|
||||
expect(imap_email_channel.reload.imap_inbox_synced_at).to be > mail_date
|
||||
end
|
||||
end
|
||||
end
|
93
spec/mailboxes/imap/imap_mailbox_spec.rb
Normal file
93
spec/mailboxes/imap/imap_mailbox_spec.rb
Normal file
|
@ -0,0 +1,93 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Imap::ImapMailbox, type: :mailbox do
|
||||
include ActionMailbox::TestHelper
|
||||
|
||||
describe 'add mail as a new conversation in the email inbox' do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, email: 'agent@example.com', account: account) }
|
||||
let(:channel) do
|
||||
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com',
|
||||
imap_port: 993, imap_email: 'imap@gmail.com', imap_password: 'password',
|
||||
account: account)
|
||||
end
|
||||
let(:inbox) { create(:inbox, channel: channel, account: account) }
|
||||
let!(:contact) { create(:contact, email: 'email@gmail.com', phone_number: '+919584546666', account: account, identifier: '123') }
|
||||
let(:conversation) { Conversation.where(inbox_id: channel.inbox).last }
|
||||
let(:class_instance) { described_class.new }
|
||||
|
||||
before do
|
||||
create(:contact_inbox, contact_id: contact.id, inbox_id: channel.inbox.id)
|
||||
end
|
||||
|
||||
context 'when a new email from non existing contact' do
|
||||
let(:inbound_mail) { create_inbound_email_from_mail(from: 'testemail@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }
|
||||
|
||||
it 'creates the contact and conversation with message' do
|
||||
class_instance.process(inbound_mail.mail, channel)
|
||||
expect(conversation.contact.email).to eq(inbound_mail.mail.from.first)
|
||||
expect(conversation.additional_attributes['source']).to eq('email')
|
||||
expect(conversation.messages.empty?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a new email from existing contact' do
|
||||
let(:inbound_mail) { create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!') }
|
||||
|
||||
it 'creates a new conversation with message' do
|
||||
class_instance.process(inbound_mail.mail, channel)
|
||||
expect(conversation.contact.email).to eq(contact.email)
|
||||
expect(conversation.additional_attributes['source']).to eq('email')
|
||||
expect(conversation.messages.empty?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a reply for existing email conversation' do
|
||||
let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) }
|
||||
let(:reply_mail) do
|
||||
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
|
||||
end
|
||||
|
||||
it 'appends new email to the existing conversation' do
|
||||
create(
|
||||
:message,
|
||||
content: 'Incoming Message',
|
||||
message_type: 'incoming',
|
||||
inbox: inbox,
|
||||
account: account,
|
||||
conversation: prev_conversation
|
||||
)
|
||||
create(
|
||||
:message,
|
||||
content: 'Outgoing Message',
|
||||
message_type: 'outgoing',
|
||||
inbox: inbox,
|
||||
source_id: 'test-in-reply-to',
|
||||
account: account,
|
||||
conversation: prev_conversation
|
||||
)
|
||||
|
||||
expect(prev_conversation.messages.size).to eq(2)
|
||||
|
||||
class_instance.process(reply_mail.mail, channel)
|
||||
|
||||
expect(prev_conversation.messages.size).to eq(3)
|
||||
expect(prev_conversation.messages.last.content_attributes['email']['from']).to eq(reply_mail.mail.from)
|
||||
expect(prev_conversation.messages.last.content_attributes['email']['to']).to eq(reply_mail.mail.to)
|
||||
expect(prev_conversation.messages.last.content_attributes['email']['subject']).to eq(reply_mail.mail.subject)
|
||||
expect(prev_conversation.messages.last.content_attributes['email']['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a reply for non existing email conversation' do
|
||||
let(:reply_mail) do
|
||||
create_inbound_email_from_mail(from: 'email@gmail.com', to: 'imap@gmail.com', subject: 'Hello!', in_reply_to: 'test-in-reply-to')
|
||||
end
|
||||
|
||||
it 'creates new email conversation with incoming in-reply-to' do
|
||||
class_instance.process(reply_mail.mail, channel)
|
||||
expect(conversation.additional_attributes['in_reply_to']).to eq(reply_mail.mail.in_reply_to)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -154,6 +154,32 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when smtp enabled for email channel' do
|
||||
let(:smtp_email_channel) do
|
||||
create(:channel_email, smtp_enabled: true, smtp_address: 'smtp.gmail.com', smtp_port: 587, smtp_email: 'smtp@gmail.com',
|
||||
smtp_password: 'password', smtp_domain: 'smtp.gmail.com', account: account)
|
||||
end
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: smtp_email_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use smtp mail server' do
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail.delivery_method.settings.empty?).to be false
|
||||
expect(mail.delivery_method.settings[:address]).to eq 'smtp.gmail.com'
|
||||
expect(mail.delivery_method.settings[:port]).to eq 587
|
||||
end
|
||||
end
|
||||
|
||||
context 'when smtp disabled for email channel', :test do
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: email_channel.inbox, account: account).reload }
|
||||
let(:message) { create(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
|
||||
|
||||
it 'use default mail server' do
|
||||
mail = described_class.email_reply(message)
|
||||
expect(mail.delivery_method.settings).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom domain and email is not enabled' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, user: agent, inbox: inbox) }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue