refactor(email): tighten inbox setup flow

This commit is contained in:
Sojan Jose
2026-05-31 14:01:03 +05:30
parent 8ac5af649a
commit 6c2aed2f40
4 changed files with 279 additions and 270 deletions
@@ -41,7 +41,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
name: inbox_name(channel),
channel: channel
}.merge(
permitted_params.except(:channel)
permitted_params.except(:channel, :imap_fetch_interval)
)
)
@inbox.save!
@@ -192,7 +192,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def permitted_params(channel_attributes = [])
# We will remove this line after fixing https://linear.app/chatwoot/issue/CW-1567/null-value-passed-as-null-string-to-backend
params.each { |k, v| params[k] = params[k] == 'null' ? nil : v }
params.permit(*inbox_attributes, channel: [:type, *channel_attributes])
params.permit(:imap_fetch_interval, *inbox_attributes, channel: [:type, *channel_attributes])
end
def channel_type_from_params
@@ -1196,7 +1196,7 @@
},
"OTHER_PROVIDERS": {
"TITLE": "Other Providers",
"DESCRIPTION": "Connect with Other Providers"
"DESCRIPTION": "Use IMAP/SMTP"
}
},
"CHANNELS": {
@@ -25,11 +25,10 @@ const isInboundEmailEnabled = computed(
() => currentAccount.value?.features?.[FEATURE_FLAGS.INBOUND_EMAILS]
);
const isForwardingEnabled = computed(() => {
return (
const isForwardingEnabled = computed(
() =>
isInboundEmailEnabled.value && globalConfig.value.inboundEmailDomainPresent
);
});
);
const emailProviderList = computed(() => {
return [
@@ -1,335 +1,345 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { computed, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useVuelidate } from '@vuelidate/core';
import {
email as emailRule,
required,
requiredIf,
} from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import { required, requiredIf, email } from '@vuelidate/validators';
import router from '../../../../../index';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import PageHeader from '../../../SettingsSubPageHeader.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import Select from 'dashboard/components-next/select/Select.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
PageHeader,
NextButton,
},
setup() {
return { v$: useVuelidate() };
},
data() {
return {
channelName: '',
email: '',
imapAddress: '',
imapPort: 993,
imapLogin: '',
imapPassword: '',
imapEnableSSL: true,
imapAuthentication: 'plain',
smtpAddress: '',
smtpPort: 587,
smtpLogin: '',
smtpPassword: '',
smtpDomain: '',
smtpEncryption: 'starttls',
smtpVerifyMode: 'none',
smtpAuthentication: 'login',
imapFetchInterval: 1,
imapAuthMechanisms: ['plain', 'login', 'cram-md5'],
smtpVerifyModes: ['none', 'peer', 'client_once', 'fail_if_no_peer_cert'],
smtpAuthMechanisms: [
'plain',
'login',
'cram-md5',
'xoauth',
'xoauth2',
'ntlm',
'gssapi',
],
};
},
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
}),
isSMTPConfigured() {
return [
this.smtpAddress,
this.smtpLogin,
this.smtpPassword,
this.smtpDomain,
].some(value => value?.trim());
},
},
validations: {
channelName: { required },
email: { required, email },
imapAddress: { required },
imapPort: { required },
imapLogin: { required },
imapPassword: { required },
smtpAddress: { required: requiredIf('isSMTPConfigured') },
smtpPort: { required: requiredIf('isSMTPConfigured') },
smtpLogin: { required: requiredIf('isSMTPConfigured') },
smtpPassword: { required: requiredIf('isSMTPConfigured') },
smtpDomain: { required: requiredIf('isSMTPConfigured') },
},
watch: {
email(value) {
const domain = value.split('@')[1] || '';
this.imapLogin = this.imapLogin || value;
this.smtpLogin = this.smtpLogin || value;
this.smtpDomain = this.smtpDomain || domain;
},
},
methods: {
async createChannel() {
this.v$.$touch();
if (this.v$.$invalid) {
return;
}
const IMAP_AUTH_OPTIONS = ['plain', 'login', 'cram-md5'];
const SMTP_AUTH_OPTIONS = [
'plain',
'login',
'cram-md5',
'xoauth',
'xoauth2',
'ntlm',
'gssapi',
];
const SMTP_VERIFY_OPTIONS = [
'none',
'peer',
'client_once',
'fail_if_no_peer_cert',
];
try {
const emailChannel = await this.$store.dispatch(
'inboxes/createChannel',
{
name: this.channelName?.trim(),
imap_fetch_interval: this.imapFetchInterval,
channel: {
type: 'email',
email: this.email,
imap_enabled: true,
imap_address: this.imapAddress,
imap_port: this.imapPort,
imap_login: this.imapLogin,
imap_password: this.imapPassword,
imap_enable_ssl: this.imapEnableSSL,
imap_authentication: this.imapAuthentication,
smtp_enabled: this.isSMTPConfigured,
smtp_address: this.smtpAddress,
smtp_port: this.smtpPort,
smtp_login: this.smtpLogin,
smtp_password: this.smtpPassword,
smtp_domain: this.smtpDomain,
smtp_enable_ssl_tls: this.smtpEncryption === 'ssl',
smtp_enable_starttls_auto: this.smtpEncryption === 'starttls',
smtp_openssl_verify_mode: this.smtpVerifyMode,
smtp_authentication: this.smtpAuthentication,
},
}
);
const { t } = useI18n();
const router = useRouter();
const store = useStore();
const uiFlags = useMapGetter('inboxes/getUIFlags');
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: emailChannel.id,
},
});
} catch (error) {
useAlert(
error?.message ||
this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.API.ERROR_MESSAGE')
);
}
},
const state = reactive({
channelName: '',
email: '',
imapAddress: '',
imapPort: 993,
imapLogin: '',
imapPassword: '',
imapEnableSSL: true,
imapAuthentication: 'plain',
imapFetchInterval: 1,
smtpAddress: '',
smtpPort: 587,
smtpLogin: '',
smtpPassword: '',
smtpDomain: '',
smtpEncryption: 'starttls',
smtpVerifyMode: 'none',
smtpAuthentication: 'login',
});
const selectOptions = options =>
options.map(value => ({ label: value, value }));
const importWindowOptions = computed(() => [
{
value: 1,
label: t('INBOX_MGMT.ADD.EMAIL_CHANNEL.IMPORT_OPTIONS.ONE_DAY'),
},
{
value: 7,
label: t('INBOX_MGMT.ADD.EMAIL_CHANNEL.IMPORT_OPTIONS.SEVEN_DAYS'),
},
{
value: 30,
label: t('INBOX_MGMT.ADD.EMAIL_CHANNEL.IMPORT_OPTIONS.THIRTY_DAYS'),
},
]);
const isSMTPConfigured = computed(() =>
[
state.smtpAddress,
state.smtpLogin,
state.smtpPassword,
state.smtpDomain,
].some(value => value?.trim())
);
const smtpRequired = requiredIf(() => isSMTPConfigured.value);
const validationRules = {
channelName: { required },
email: { required, email: emailRule },
imapAddress: { required },
imapPort: { required },
imapLogin: { required },
imapPassword: { required },
smtpAddress: { required: smtpRequired },
smtpPort: { required: smtpRequired },
smtpLogin: { required: smtpRequired },
smtpPassword: { required: smtpRequired },
smtpDomain: { required: smtpRequired },
};
const v$ = useVuelidate(validationRules, state);
const fieldState = field => (v$.value[field]?.$error ? 'error' : 'info');
watch(
() => state.email,
value => {
state.imapLogin = state.imapLogin || value;
}
);
const buildChannelPayload = () => ({
type: 'email',
email: state.email,
imap_enabled: true,
imap_address: state.imapAddress,
imap_port: state.imapPort,
imap_login: state.imapLogin,
imap_password: state.imapPassword,
imap_enable_ssl: state.imapEnableSSL,
imap_authentication: state.imapAuthentication,
smtp_enabled: isSMTPConfigured.value,
smtp_address: state.smtpAddress,
smtp_port: state.smtpPort,
smtp_login: state.smtpLogin,
smtp_password: state.smtpPassword,
smtp_domain: state.smtpDomain,
smtp_enable_ssl_tls: state.smtpEncryption === 'ssl',
smtp_enable_starttls_auto: state.smtpEncryption === 'starttls',
smtp_openssl_verify_mode: state.smtpVerifyMode,
smtp_authentication: state.smtpAuthentication,
});
async function createChannel() {
const isValid = await v$.value.$validate();
if (!isValid) return;
try {
const emailChannel = await store.dispatch('inboxes/createChannel', {
name: state.channelName.trim(),
imap_fetch_interval: state.imapFetchInterval,
channel: buildChannelPayload(),
});
router.replace({
name: 'settings_inboxes_add_agents',
params: { page: 'new', inbox_id: emailChannel.id },
});
} catch (error) {
useAlert(
error?.message || t('INBOX_MGMT.ADD.EMAIL_CHANNEL.API.ERROR_MESSAGE')
);
}
}
</script>
<template>
<div class="h-full w-full p-6 col-span-6">
<div class="overflow-auto col-span-6 p-6 w-full h-full">
<PageHeader
:header-title="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.DESC')"
:header-title="t('INBOX_MGMT.ADD.EMAIL_CHANNEL.TITLE')"
:header-content="t('INBOX_MGMT.ADD.EMAIL_CHANNEL.DESC')"
/>
<form class="max-w-3xl" @submit.prevent="createChannel">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<woot-input
v-model="channelName"
:class="{ error: v$.channelName.$error }"
:label="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.LABEL')"
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<Input
v-model="state.channelName"
:label="t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.LABEL')"
:placeholder="
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
"
:message-type="fieldState('channelName')"
@blur="v$.channelName.$touch"
/>
<woot-input
v-model="email"
:class="{ error: v$.email.$error }"
:label="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.LABEL')"
:placeholder="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.PLACEHOLDER')"
<Input
v-model="state.email"
:label="t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.LABEL')"
:placeholder="t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.PLACEHOLDER')"
:message-type="fieldState('email')"
@blur="v$.email.$touch"
/>
</div>
<div class="mt-6 pt-5 border-t border-n-weak">
<section class="pt-5 mt-6 border-t border-n-weak">
<h3 class="mb-1 text-sm font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.IMAP.TITLE') }}
{{ t('INBOX_MGMT.IMAP.TITLE') }}
</h3>
<p class="mb-4 text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.IMAP.CREATE_HELP') }}
{{ t('INBOX_MGMT.IMAP.CREATE_HELP') }}
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<woot-input
v-model="imapAddress"
:class="{ error: v$.imapAddress.$error }"
:label="$t('INBOX_MGMT.IMAP.ADDRESS.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.ADDRESS.PLACE_HOLDER')"
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<Input
v-model="state.imapAddress"
:label="t('INBOX_MGMT.IMAP.ADDRESS.LABEL')"
:placeholder="t('INBOX_MGMT.IMAP.ADDRESS.PLACE_HOLDER')"
:message-type="fieldState('imapAddress')"
@blur="v$.imapAddress.$touch"
/>
<div>
<woot-input
v-model="imapPort"
<Input
v-model="state.imapPort"
type="number"
:class="{ error: v$.imapPort.$error }"
:label="$t('INBOX_MGMT.IMAP.PORT.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.PORT.PLACE_HOLDER')"
:label="t('INBOX_MGMT.IMAP.PORT.LABEL')"
:placeholder="t('INBOX_MGMT.IMAP.PORT.PLACE_HOLDER')"
:message-type="fieldState('imapPort')"
@blur="v$.imapPort.$touch"
/>
<label
for="imap-enable-ssl"
class="flex items-center -mt-2 mb-4 text-sm font-medium text-n-slate-12"
class="flex items-center gap-2 -mt-1 mb-4 text-sm font-medium text-n-slate-12"
>
<input
id="imap-enable-ssl"
v-model="imapEnableSSL"
v-model="state.imapEnableSSL"
type="checkbox"
class="ltr:mr-2 rtl:ml-2"
/>
{{ $t('INBOX_MGMT.IMAP.ENABLE_SSL') }}
{{ t('INBOX_MGMT.IMAP.ENABLE_SSL') }}
</label>
</div>
<woot-input
v-model="imapLogin"
:class="{ error: v$.imapLogin.$error }"
:label="$t('INBOX_MGMT.IMAP.LOGIN.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.LOGIN.PLACE_HOLDER')"
<Input
v-model="state.imapLogin"
:label="t('INBOX_MGMT.IMAP.LOGIN.LABEL')"
:placeholder="t('INBOX_MGMT.IMAP.LOGIN.PLACE_HOLDER')"
:message-type="fieldState('imapLogin')"
@blur="v$.imapLogin.$touch"
/>
<woot-input
v-model="imapPassword"
<Input
v-model="state.imapPassword"
type="password"
:class="{ error: v$.imapPassword.$error }"
:label="$t('INBOX_MGMT.IMAP.PASSWORD.LABEL')"
:placeholder="$t('INBOX_MGMT.IMAP.PASSWORD.PLACE_HOLDER')"
:label="t('INBOX_MGMT.IMAP.PASSWORD.LABEL')"
:placeholder="t('INBOX_MGMT.IMAP.PASSWORD.PLACE_HOLDER')"
:message-type="fieldState('imapPassword')"
@blur="v$.imapPassword.$touch"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<label>
{{ $t('INBOX_MGMT.IMAP.AUTH_MECHANISM') }}
<select v-model="imapAuthentication">
<option
v-for="authentication in imapAuthMechanisms"
:key="authentication"
:value="authentication"
>
{{ authentication }}
</option>
</select>
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<label class="flex flex-col gap-1">
<span class="text-heading-3 text-n-slate-12">
{{ t('INBOX_MGMT.IMAP.AUTH_MECHANISM') }}
</span>
<Select
v-model="state.imapAuthentication"
:options="selectOptions(IMAP_AUTH_OPTIONS)"
/>
</label>
<label>
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FETCH_EMAILS_FROM') }}
<select v-model="imapFetchInterval">
<option :value="1">
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.IMPORT_OPTIONS.ONE_DAY') }}
</option>
<option :value="7">
{{
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.IMPORT_OPTIONS.SEVEN_DAYS')
}}
</option>
<option :value="30">
{{
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.IMPORT_OPTIONS.THIRTY_DAYS')
}}
</option>
</select>
<label class="flex flex-col gap-1">
<span class="text-heading-3 text-n-slate-12">
{{ t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FETCH_EMAILS_FROM') }}
</span>
<Select
v-model="state.imapFetchInterval"
:options="importWindowOptions"
/>
</label>
</div>
</div>
</section>
<div class="mt-6 pt-5 border-t border-n-weak">
<div class="mb-4">
<h3 class="mb-1 text-sm font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.SMTP.TITLE') }}
</h3>
<p class="text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.SMTP.CREATE_HELP') }}
</p>
</div>
<section class="pt-5 mt-6 border-t border-n-weak">
<h3 class="mb-1 text-sm font-medium text-n-slate-12">
{{ t('INBOX_MGMT.SMTP.TITLE') }}
</h3>
<p class="mb-4 text-sm text-n-slate-11">
{{ t('INBOX_MGMT.SMTP.CREATE_HELP') }}
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<woot-input
v-model="smtpAddress"
:class="{ error: v$.smtpAddress.$error }"
:label="$t('INBOX_MGMT.SMTP.ADDRESS.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.ADDRESS.PLACE_HOLDER')"
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<Input
v-model="state.smtpAddress"
:label="t('INBOX_MGMT.SMTP.ADDRESS.LABEL')"
:placeholder="t('INBOX_MGMT.SMTP.ADDRESS.PLACE_HOLDER')"
:message-type="fieldState('smtpAddress')"
@blur="v$.smtpAddress.$touch"
/>
<woot-input
v-model="smtpPort"
<Input
v-model="state.smtpPort"
type="number"
:class="{ error: v$.smtpPort.$error }"
:label="$t('INBOX_MGMT.SMTP.PORT.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.PORT.PLACE_HOLDER')"
:label="t('INBOX_MGMT.SMTP.PORT.LABEL')"
:placeholder="t('INBOX_MGMT.SMTP.PORT.PLACE_HOLDER')"
:message-type="fieldState('smtpPort')"
@blur="v$.smtpPort.$touch"
/>
<woot-input
v-model="smtpLogin"
:class="{ error: v$.smtpLogin.$error }"
:label="$t('INBOX_MGMT.SMTP.LOGIN.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.LOGIN.PLACE_HOLDER')"
<Input
v-model="state.smtpLogin"
:label="t('INBOX_MGMT.SMTP.LOGIN.LABEL')"
:placeholder="t('INBOX_MGMT.SMTP.LOGIN.PLACE_HOLDER')"
:message-type="fieldState('smtpLogin')"
@blur="v$.smtpLogin.$touch"
/>
<woot-input
v-model="smtpPassword"
<Input
v-model="state.smtpPassword"
type="password"
:class="{ error: v$.smtpPassword.$error }"
:label="$t('INBOX_MGMT.SMTP.PASSWORD.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.PASSWORD.PLACE_HOLDER')"
:label="t('INBOX_MGMT.SMTP.PASSWORD.LABEL')"
:placeholder="t('INBOX_MGMT.SMTP.PASSWORD.PLACE_HOLDER')"
:message-type="fieldState('smtpPassword')"
@blur="v$.smtpPassword.$touch"
/>
<woot-input
v-model="smtpDomain"
:class="{ error: v$.smtpDomain.$error }"
:label="$t('INBOX_MGMT.SMTP.DOMAIN.LABEL')"
:placeholder="$t('INBOX_MGMT.SMTP.DOMAIN.PLACE_HOLDER')"
<Input
v-model="state.smtpDomain"
:label="t('INBOX_MGMT.SMTP.DOMAIN.LABEL')"
:placeholder="t('INBOX_MGMT.SMTP.DOMAIN.PLACE_HOLDER')"
:message-type="fieldState('smtpDomain')"
@blur="v$.smtpDomain.$touch"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<label>
{{ $t('INBOX_MGMT.SMTP.ENCRYPTION') }}
<select v-model="smtpEncryption">
<option value="starttls">
{{ $t('INBOX_MGMT.SMTP.START_TLS') }}
</option>
<option value="ssl">{{ $t('INBOX_MGMT.SMTP.SSL_TLS') }}</option>
</select>
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<label class="flex flex-col gap-1 mb-4">
<span class="text-heading-3 text-n-slate-12">
{{ t('INBOX_MGMT.SMTP.ENCRYPTION') }}
</span>
<Select
v-model="state.smtpEncryption"
:options="[
{ value: 'starttls', label: t('INBOX_MGMT.SMTP.START_TLS') },
{ value: 'ssl', label: t('INBOX_MGMT.SMTP.SSL_TLS') },
]"
/>
</label>
<label>
{{ $t('INBOX_MGMT.SMTP.OPEN_SSL_VERIFY_MODE') }}
<select v-model="smtpVerifyMode">
<option v-for="mode in smtpVerifyModes" :key="mode" :value="mode">
{{ mode }}
</option>
</select>
<label class="flex flex-col gap-1 mb-4">
<span class="text-heading-3 text-n-slate-12">
{{ t('INBOX_MGMT.SMTP.OPEN_SSL_VERIFY_MODE') }}
</span>
<Select
v-model="state.smtpVerifyMode"
:options="selectOptions(SMTP_VERIFY_OPTIONS)"
/>
</label>
<label>
{{ $t('INBOX_MGMT.SMTP.AUTH_MECHANISM') }}
<select v-model="smtpAuthentication">
<option
v-for="authentication in smtpAuthMechanisms"
:key="authentication"
:value="authentication"
>
{{ authentication }}
</option>
</select>
<label class="flex flex-col gap-1">
<span class="text-heading-3 text-n-slate-12">
{{ t('INBOX_MGMT.SMTP.AUTH_MECHANISM') }}
</span>
<Select
v-model="state.smtpAuthentication"
:options="selectOptions(SMTP_AUTH_OPTIONS)"
/>
</label>
</div>
</div>
</section>
<div class="w-full mt-6">
<NextButton
@@ -337,7 +347,7 @@ export default {
type="submit"
solid
blue
:label="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.SUBMIT_BUTTON')"
:label="t('INBOX_MGMT.ADD.EMAIL_CHANNEL.SUBMIT_BUTTON')"
/>
</div>
</form>