Browse Source

Desktop-Rewrite: Guided Setup - Email Notification and Channel handling.

Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
Co-authored-by: Mantas Masalskis <mm@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Florian Liebe 1 year ago
parent
commit
b9bdcd0325

+ 1 - 1
app/assets/javascripts/app/controllers/getting_started/channel_email.coffee

@@ -230,7 +230,7 @@ class GettingStartedChannelEmail extends App.ControllerWizardFullScreen
           @account.inbound = params
 
           if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
-            @probeInboundMessagesFound(data, true)
+            @probeInboundMessagesFound(data, false)
             @probeInboundArchive(data)
           else
             @showSlide('js-outbound')

+ 12 - 54
app/controllers/channels_email_controller.rb

@@ -137,45 +137,14 @@ class ChannelsEmailController < ApplicationController
       return
     end
 
-    # create new account
-    channel = Channel.create(
-      area:         'Email::Account',
-      options:      {
-        inbound:  params[:inbound].to_h,
-        outbound: params[:outbound].to_h,
-      },
-      group_id:     params[:group_id],
-      last_log_in:  nil,
-      last_log_out: nil,
-      status_in:    'ok',
-      status_out:   'ok',
-      active:       true,
+    ::Service::Channel::Email::Add.new.execute(
+      inbound_configuration:  params[:inbound].to_h,
+      outbound_configuration: params[:outbound].to_h,
+      group:                  ::Group.find(params[:group_id]),
+      email_address:          email,
+      email_realname:         params[:meta][:realname],
     )
 
-    # remember address && set channel for email address
-    address = EmailAddress.find_by(email: email)
-
-    # on initial setup, use placeholder email address
-    if Channel.count == 1
-      address = EmailAddress.first
-    end
-
-    if address
-      address.update!(
-        name:       params[:meta][:realname],
-        email:      email,
-        active:     true,
-        channel_id: channel.id,
-      )
-    else
-      EmailAddress.create(
-        name:       params[:meta][:realname],
-        email:      email,
-        active:     true,
-        channel_id: channel.id,
-      )
-    end
-
     render json: result
   end
 
@@ -221,24 +190,13 @@ class ChannelsEmailController < ApplicationController
 
     # save settings
     if result[:result] == 'ok'
-
-      Channel.where(area: 'Email::Notification').each do |channel|
-        active = false
-        if adapter.match?(%r{^#{channel.options[:outbound][:adapter]}$}i)
-          active = true
-          channel.options = {
-            outbound: {
-              adapter: adapter,
-              options: params[:options].to_h,
-            },
-          }
-          channel.status_out   = 'ok'
-          channel.last_log_out = nil
-        end
-        channel.active = active
-        channel.save
-      end
+      Service::System::SetEmailNotificationConfiguration
+        .new(
+          adapter:,
+          new_configuration: params[:options].to_h
+        ).execute
     end
+
     render json: result
   end
 

+ 2 - 7
app/frontend/apps/desktop/components/CommonButton/CommonButton.vue

@@ -102,12 +102,7 @@ const paddingClasses = computed(() => {
 const disabledClasses = computed(() => {
   if (!props.disabled) return []
 
-  return [
-    '!bg-green-100',
-    'dark:!bg-gray-400',
-    'text-stone-200',
-    'dark:text-neutral-500',
-  ]
+  return ['opacity-30', 'pointer-events-none']
 })
 
 const borderRadiusClass = computed(() => {
@@ -152,7 +147,7 @@ const iconSizeClass = computed(() => {
     ]"
     :type="type"
     :form="form"
-    :disabled="disabled"
+    :aria-disabled="disabled ? 'true' : undefined"
   >
     <CommonIcon
       v-if="prefixIcon"

+ 1 - 1
app/frontend/apps/desktop/components/CommonButton/__tests__/CommonButton.spec.ts

@@ -50,7 +50,7 @@ describe('CommonButton.vue', () => {
       },
     })
 
-    expect(view.getByRole('button')).toHaveAttribute('disabled')
+    expect(view.getByRole('button')).toBeDisabled()
   })
 
   it('supports block prop', async () => {

+ 0 - 1
app/frontend/apps/desktop/components/CommonLoader/CommonLoader.vue

@@ -4,7 +4,6 @@
 /* eslint-disable vue/no-v-html */
 
 import { markup } from '#shared/utils/markup.ts'
-import CommonAlert from '#shared/components/CommonAlert/CommonAlert.vue'
 
 interface Props {
   loading?: boolean

+ 60 - 0
app/frontend/apps/desktop/entities/channel-email/composables/useEmailAccountForm.ts

@@ -0,0 +1,60 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import type { ShallowRef } from 'vue'
+import { shallowRef } from 'vue'
+
+import type { FormRef } from '#shared/components/Form/types.ts'
+import { useForm } from '#shared/components/Form/useForm.ts'
+
+import type { EmailAccountData } from '../types/email-account.ts'
+
+export const useEmailAccountForm = () => {
+  const formEmailAccount: ShallowRef<FormRef | undefined> = shallowRef()
+
+  const emailAccountSchema = [
+    {
+      isLayout: true,
+      element: 'div',
+      attrs: {
+        class: 'grid grid-cols-1 gap-y-2.5 gap-x-3',
+      },
+      children: [
+        {
+          name: 'realname',
+          label: __('Full Name'),
+          type: 'text',
+          props: {
+            placeholder: __('Organization Support'),
+          },
+          required: true,
+        },
+        {
+          name: 'email',
+          label: __('Email Address'),
+          type: 'email',
+          props: {},
+          validation: 'email',
+          required: true,
+        },
+        {
+          name: 'password',
+          label: __('Password'),
+          type: 'password',
+          props: {},
+          required: true,
+        },
+      ],
+    },
+  ]
+
+  const { values, formSetErrors, updateFieldValues } =
+    useForm<EmailAccountData>(formEmailAccount)
+
+  return {
+    formEmailAccount,
+    emailAccountSchema,
+    formEmailAccountValues: values,
+    updateEmailAccountFieldValues: updateFieldValues,
+    formEmailAccountSetErrors: formSetErrors,
+  }
+}

+ 367 - 0
app/frontend/apps/desktop/entities/channel-email/composables/useEmailChannelConfiguration.ts

@@ -0,0 +1,367 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import type { SetNonNullable, SetOptional } from 'type-fest'
+import type { Ref } from 'vue'
+import { computed, ref, watch } from 'vue'
+
+import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
+import { useDebouncedLoading } from '#shared/composables/useDebouncedLoading.ts'
+import type { FormSubmitData } from '#shared/components/Form/types.ts'
+import type { MutationSendError } from '#shared/types/error.ts'
+import type {
+  ChannelEmailInboundConfiguration,
+  ChannelEmailOutboundConfiguration,
+} from '#shared/graphql/types.ts'
+import UserError from '#shared/errors/UserError.ts'
+import { i18n } from '#shared/i18n.ts'
+
+import { useChannelEmailValidateConfigurationRoundtripMutation } from '#desktop/entities/channel-email/graphql/mutations/channelEmailValidateConfigurationRoundtrip.api.ts'
+import { useChannelEmailAddMutation } from '#desktop/entities/channel-email/graphql/mutations/channelEmailAdd.api.ts'
+
+import type {
+  EmailChannelSteps,
+  EmailChannelForms,
+} from '../types/email-channel.ts'
+import type { EmailAccountData } from '../types/email-account.ts'
+import type {
+  UpdateMetaInformationInboundFunction,
+  EmailInboundMetaInformation,
+  EmailOutboundData,
+  EmailInboundData,
+  EmailInboundMessagesData,
+} from '../types/email-inbound-outbound.ts'
+import { useChannelEmailGuessConfigurationMutation } from '../graphql/mutations/channelEmailGuessConfiguration.api.ts'
+import { useChannelEmailValidateConfigurationInboundMutation } from '../graphql/mutations/channelEmailValidateConfigurationInbound.api.ts'
+import { useChannelEmailValidateConfigurationOutboundMutation } from '../graphql/mutations/channelEmailValidateConfigurationOutbound.api.ts'
+
+export const useEmailChannelConfiguration = (
+  emailChannelForms: EmailChannelForms,
+  metaInformationInbound: Ref<Maybe<EmailInboundMetaInformation>>,
+  updateMetaInformationInbound: UpdateMetaInformationInboundFunction,
+  onSuccessCallback: () => void,
+) => {
+  const { loading, debouncedLoading } = useDebouncedLoading()
+  const activeStep = ref<EmailChannelSteps>('account')
+  const pendingActiveStep = ref<Maybe<EmailChannelSteps>>(null)
+
+  const setActiveStep = (nextStep: EmailChannelSteps) => {
+    if (!debouncedLoading.value) {
+      activeStep.value = nextStep
+      return
+    }
+
+    console.log('pendingActiveStep', nextStep)
+    pendingActiveStep.value = nextStep
+  }
+
+  watch(debouncedLoading, (newValue: boolean) => {
+    console.log('activeStep', activeStep.value)
+    if (!newValue && pendingActiveStep.value) {
+      console.log('SWITCH', pendingActiveStep.value)
+      activeStep.value = pendingActiveStep.value
+      pendingActiveStep.value = null
+    }
+  })
+
+  const stepTitle = computed(() => {
+    switch (activeStep.value) {
+      case 'inbound':
+      case 'inbound-messages':
+        return __('Email Inbound')
+      case 'outbound':
+        return __('Email Outbound')
+      default:
+        return __('Email Account')
+    }
+  })
+
+  const activeForm = computed(() => {
+    switch (activeStep.value) {
+      case 'inbound':
+        return emailChannelForms.emailInbound.form.value
+      case 'inbound-messages':
+        return emailChannelForms.emailInboundMessages.form.value
+      case 'outbound':
+        return emailChannelForms.emailOutbound.form.value
+      default:
+        return emailChannelForms.emailAccount.form.value
+    }
+  })
+
+  const validateConfigurationRoundtripAndChannelAdd = async (
+    account: EmailAccountData,
+    inboundConfiguration: EmailInboundData,
+    outboundConfiguration: EmailOutboundData,
+  ) => {
+    const validateConfigurationRoundtripMutation = new MutationHandler(
+      useChannelEmailValidateConfigurationRoundtripMutation(),
+    )
+    const addEmailChannelMutation = new MutationHandler(
+      useChannelEmailAddMutation(),
+    )
+
+    // Transform port field to real number for usage in the mutation.
+    inboundConfiguration.port = Number(inboundConfiguration.port)
+    outboundConfiguration.port = Number(outboundConfiguration.port)
+
+    // Extend inbound configuration with archive information when needed.
+    if (metaInformationInbound.value?.archive) {
+      inboundConfiguration = {
+        ...inboundConfiguration,
+        archive: true,
+        archiveBefore: metaInformationInbound.value.archiveBefore,
+      }
+    }
+
+    try {
+      const roundTripResult = await validateConfigurationRoundtripMutation.send(
+        {
+          inboundConfiguration,
+          outboundConfiguration,
+          emailAddress: account.email,
+        },
+      )
+
+      if (
+        roundTripResult?.channelEmailValidateConfigurationRoundtrip?.success
+      ) {
+        try {
+          const addChannelResult = await addEmailChannelMutation.send({
+            input: {
+              inboundConfiguration,
+              outboundConfiguration,
+              emailAddress: account.email,
+              emailRealname: account.realname,
+            },
+          })
+
+          if (addChannelResult?.channelEmailAdd?.channel) {
+            onSuccessCallback()
+          }
+        } catch (errors) {
+          emailChannelForms.emailAccount.setErrors(errors as MutationSendError)
+          setActiveStep('account')
+        }
+      }
+    } catch (errors) {
+      if (
+        errors instanceof UserError &&
+        Object.keys(errors.getFieldErrorList()).length > 0
+      ) {
+        if (
+          Object.keys(errors.getFieldErrorList()).some((key) =>
+            key.startsWith('outbound'),
+          )
+        ) {
+          setActiveStep('outbound')
+          emailChannelForms.emailOutbound.setErrors(errors as MutationSendError)
+        } else {
+          setActiveStep('inbound')
+          emailChannelForms.emailInbound.setErrors(errors as MutationSendError)
+        }
+        return
+      }
+
+      emailChannelForms.emailAccount.setErrors(
+        new UserError([
+          {
+            message: i18n.t(
+              'Email sending and receiving could not be verified. Please check your settings.',
+            ),
+          },
+        ]),
+      )
+      setActiveStep('account')
+    }
+  }
+
+  const guessEmailAccount = (data: FormSubmitData<EmailAccountData>) => {
+    loading.value = true
+
+    const guessConfigurationMutation = new MutationHandler(
+      useChannelEmailGuessConfigurationMutation(),
+    )
+
+    return guessConfigurationMutation
+      .send({
+        emailAddress: data.email,
+        password: data.password,
+      })
+      .then(async (result) => {
+        if (
+          result?.channelEmailGuessConfiguration?.result.inboundConfiguration &&
+          result?.channelEmailGuessConfiguration?.result.outboundConfiguration
+        ) {
+          const inboundConfiguration = result.channelEmailGuessConfiguration
+            .result.inboundConfiguration as SetOptional<
+            SetNonNullable<Required<ChannelEmailInboundConfiguration>>,
+            '__typename'
+          >
+          delete inboundConfiguration.__typename
+
+          const outboundConfiguration = result.channelEmailGuessConfiguration
+            .result.outboundConfiguration as SetOptional<
+            SetNonNullable<Required<ChannelEmailOutboundConfiguration>>,
+            '__typename'
+          >
+          delete outboundConfiguration.__typename
+
+          emailChannelForms.emailInbound.updateFieldValues(inboundConfiguration)
+          emailChannelForms.emailOutbound.updateFieldValues(
+            outboundConfiguration,
+          )
+
+          const mailboxStats =
+            result?.channelEmailGuessConfiguration?.result.mailboxStats
+
+          if (
+            mailboxStats?.contentMessages &&
+            mailboxStats?.contentMessages > 0
+          ) {
+            updateMetaInformationInbound(mailboxStats, 'roundtrip')
+            setActiveStep('inbound-messages')
+            return
+          }
+
+          await validateConfigurationRoundtripAndChannelAdd(
+            data,
+            inboundConfiguration,
+            outboundConfiguration,
+          )
+        } else {
+          emailChannelForms.emailInbound.updateFieldValues({
+            user: data.email,
+            password: data.password,
+          })
+          emailChannelForms.emailOutbound.updateFieldValues({
+            user: data.email,
+            password: data.password,
+          })
+
+          emailChannelForms.emailInbound.setErrors(
+            new UserError([
+              {
+                message: i18n.t(
+                  'The server settings could not be automatically detected. Please configure them manually.',
+                ),
+              },
+            ]),
+          )
+
+          setActiveStep('inbound')
+        }
+      })
+      .finally(() => {
+        loading.value = false
+      })
+  }
+
+  const validateEmailInbound = (data: FormSubmitData<EmailInboundData>) => {
+    loading.value = true
+
+    const validationConfigurationInbound = new MutationHandler(
+      useChannelEmailValidateConfigurationInboundMutation(),
+    )
+
+    return validationConfigurationInbound
+      .send({
+        inboundConfiguration: {
+          ...data,
+          port: Number(data.port),
+        },
+      })
+      .then((result) => {
+        if (result?.channelEmailValidateConfigurationInbound?.success) {
+          emailChannelForms.emailOutbound.updateFieldValues({
+            host: data.host,
+            user: data.user,
+            password: data.password,
+          })
+
+          const mailboxStats =
+            result?.channelEmailValidateConfigurationInbound?.mailboxStats
+
+          if (
+            mailboxStats?.contentMessages &&
+            mailboxStats?.contentMessages > 0 &&
+            !data.keepOnServer
+          ) {
+            updateMetaInformationInbound(mailboxStats, 'outbound')
+            setActiveStep('inbound-messages')
+            return
+          }
+
+          setActiveStep('outbound')
+        }
+      })
+      .finally(() => {
+        loading.value = false
+      })
+  }
+
+  const importEmailInboundMessages = async (
+    data: FormSubmitData<EmailInboundMessagesData>,
+  ) => {
+    if (metaInformationInbound.value && data.archive) {
+      metaInformationInbound.value.archive = true
+      metaInformationInbound.value.archiveBefore = new Date().toISOString()
+    }
+
+    if (metaInformationInbound.value?.nextAction === 'outbound') {
+      setActiveStep('outbound')
+    }
+
+    if (metaInformationInbound.value?.nextAction === 'roundtrip') {
+      loading.value = true
+
+      await validateConfigurationRoundtripAndChannelAdd(
+        emailChannelForms.emailAccount.values.value,
+        emailChannelForms.emailInbound.values.value,
+        emailChannelForms.emailOutbound.values.value,
+      )
+
+      loading.value = false
+    }
+  }
+
+  const validateEmailOutbound = (data: FormSubmitData<EmailOutboundData>) => {
+    loading.value = true
+
+    const validationConfigurationOutbound = new MutationHandler(
+      useChannelEmailValidateConfigurationOutboundMutation(),
+    )
+
+    return validationConfigurationOutbound
+      .send({
+        outboundConfiguration: {
+          ...data,
+          port: Number(data.port),
+        },
+        emailAddress: emailChannelForms.emailAccount.values.value
+          ?.email as string,
+      })
+      .then(async (result) => {
+        if (result?.channelEmailValidateConfigurationOutbound?.success) {
+          await validateConfigurationRoundtripAndChannelAdd(
+            emailChannelForms.emailAccount.values.value,
+            emailChannelForms.emailInbound.values.value,
+            emailChannelForms.emailOutbound.values.value,
+          )
+        }
+      })
+      .finally(() => {
+        loading.value = false
+      })
+  }
+
+  return {
+    debouncedLoading,
+    stepTitle,
+    activeStep,
+    activeForm,
+    guessEmailAccount,
+    validateEmailInbound,
+    importEmailInboundMessages,
+    validateEmailOutbound,
+  }
+}

+ 220 - 0
app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundForm.ts

@@ -0,0 +1,220 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import type { ShallowRef } from 'vue'
+import { shallowRef, computed, ref, reactive } from 'vue'
+
+import type {
+  FormFieldValue,
+  FormRef,
+  FormSchemaField,
+} from '#shared/components/Form/types.ts'
+import { useForm } from '#shared/components/Form/useForm.ts'
+import type { ChannelEmailInboundMailboxStats } from '#shared/graphql/types.ts'
+
+import type {
+  EmailInboundData,
+  EmailInboundMetaInformation,
+  EmailInboundMetaInformationNextAction,
+} from '../types/email-inbound-outbound.ts'
+
+export const useEmailInboundForm = () => {
+  const formEmailInbound: ShallowRef<FormRef | undefined> = shallowRef()
+
+  const { values, updateFieldValues, formSetErrors } =
+    useForm<EmailInboundData>(formEmailInbound)
+
+  const metaInformationInbound = ref<Maybe<EmailInboundMetaInformation>>(null)
+
+  const updateMetaInformationInbound = (
+    data: ChannelEmailInboundMailboxStats,
+    nextAction: EmailInboundMetaInformationNextAction,
+  ) => {
+    metaInformationInbound.value = {
+      contentMessages: data.contentMessages || 0,
+      archivePossible: data.archivePossible || false,
+      archiveWeekRange: data.archiveWeekRange || 0,
+      nextAction,
+    }
+  }
+
+  const inboundSSLOptions = computed(() => {
+    const options = [
+      {
+        value: 'off',
+        label: __('No SSL'),
+      },
+      {
+        value: 'ssl',
+        label: __('SSL'),
+      },
+    ]
+
+    if (values.value.adapter === 'imap') {
+      options.push({
+        value: 'starttls',
+        label: __('STARTTLS'),
+      })
+    }
+
+    return options
+  })
+
+  const emailInboundFormChangeFields = reactive<
+    Record<string, Partial<FormSchemaField>>
+  >({
+    sslVerify: {},
+    port: {},
+  })
+
+  const emailInboundFormOnChanged = (
+    fieldName: string,
+    newValue: FormFieldValue,
+  ) => {
+    if (fieldName === 'ssl') {
+      const disabled = Boolean(newValue === 'off')
+      emailInboundFormChangeFields.sslVerify = {
+        disabled,
+      }
+
+      updateFieldValues({
+        sslVerify: !disabled,
+      })
+
+      if (newValue === 'off') {
+        emailInboundFormChangeFields.port = {
+          value: 143,
+        }
+      } else if (newValue === 'ssl') {
+        emailInboundFormChangeFields.port = {
+          value: 993,
+        }
+      }
+    }
+  }
+
+  const emailInboundSchema = [
+    {
+      isLayout: true,
+      element: 'div',
+      attrs: {
+        class: 'grid grid-cols-2 gap-y-2.5 gap-x-3',
+      },
+      children: [
+        {
+          type: 'group',
+          name: 'inbound',
+          isGroupOrList: true,
+          children: [
+            {
+              name: 'adapter',
+              label: __('Type'),
+              type: 'select',
+              outerClass: 'col-span-2',
+              required: true,
+            },
+            {
+              name: 'host',
+              label: __('Host'),
+              type: 'text',
+              outerClass: 'col-span-2',
+              props: {
+                maxLength: 120,
+              },
+              required: true,
+            },
+            {
+              name: 'user',
+              label: __('User'),
+              type: 'text',
+              outerClass: 'col-span-2',
+              props: {
+                maxLength: 120,
+              },
+              required: true,
+            },
+            {
+              name: 'password',
+              label: __('Password'),
+              type: 'password',
+              outerClass: 'col-span-2',
+              props: {
+                maxLength: 120,
+              },
+              required: true,
+            },
+            {
+              name: 'ssl',
+              label: __('SSL/STARTTLS'),
+              type: 'select',
+              outerClass: 'col-span-1',
+              value: 'ssl',
+              options: inboundSSLOptions,
+            },
+            {
+              name: 'sslVerify',
+              label: __('SSL verification'),
+              type: 'toggle',
+              outerClass: 'col-span-1',
+              wrapperClass: 'mt-6',
+              value: true,
+              props: {
+                variants: {
+                  true: 'yes',
+                  false: 'no',
+                },
+              },
+            },
+            {
+              name: 'port',
+              label: __('Port'),
+              type: 'text',
+              outerClass: 'col-span-1',
+              validation: 'number',
+              props: {
+                maxLength: 6,
+              },
+              value: 993,
+              required: true,
+            },
+            {
+              if: '$values.adapter === "imap"',
+              name: 'folder',
+              label: __('Folder'),
+              type: 'text',
+              outerClass: 'col-span-1',
+              props: {
+                maxLength: 120,
+              },
+            },
+            {
+              if: '$values.adapter === "imap"',
+              name: 'keepOnServer',
+              label: __('Keep messages on server'),
+              type: 'toggle',
+              outerClass: 'col-span-2',
+              value: false,
+              props: {
+                variants: {
+                  true: 'yes',
+                  false: 'no',
+                },
+              },
+            },
+          ],
+        },
+      ],
+    },
+  ]
+
+  return {
+    formEmailInbound,
+    emailInboundSchema,
+    formEmailInboundValues: values,
+    updateEmailInboundFieldValues: updateFieldValues,
+    formEmailInboundSetErrors: formSetErrors,
+    metaInformationInbound,
+    emailInboundFormChangeFields,
+    emailInboundFormOnChanged,
+    updateMetaInformationInbound,
+  }
+}

+ 117 - 0
app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundMessagesForm.ts

@@ -0,0 +1,117 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import type { ShallowRef, Ref } from 'vue'
+import { shallowRef, reactive } from 'vue'
+
+import type { FormRef } from '#shared/components/Form/types.ts'
+import { markup } from '#shared/utils/markup.ts'
+import { i18n } from '#shared/i18n/index.ts'
+
+import type { EmailInboundMetaInformation } from '../types/email-inbound-outbound.ts'
+
+export const useEmailInboundMessagesForm = (
+  metaInformationInbound: Ref<Maybe<EmailInboundMetaInformation>>,
+) => {
+  const formEmailInboundMessages: ShallowRef<FormRef | undefined> = shallowRef()
+
+  const emailInboundMessageSchema = [
+    {
+      isLayout: true,
+      element: 'div',
+      attrs: {
+        class: 'flex flex-col gap-y-2.5 gap-x-3',
+      },
+      children: [
+        {
+          isLayout: true,
+          component: 'CommonLabel',
+          children:
+            '$t("We have already found %s email(s) in your mailbox. We will move them all from your mailbox into Zammad.", $metaInformationInbound.contentMessages)',
+        },
+        {
+          if: '$metaInformationInbound.archivePossible === true',
+          isLayout: true,
+          element: 'div',
+          attrs: {
+            class: 'flex flex-col gap-y-2.5 gap-x-3',
+          },
+          children: [
+            {
+              isLayout: true,
+              component: 'CommonLabel',
+              children:
+                '$t(\'In addition, we have found emails in your mailbox that are older than %s weeks. You can import such emails as an "archive", which means that no notifications are sent and the tickets have the status "closed". However, you can find them in Zammad anytime using the search function.\', $metaInformationInbound.archiveWeekRange)',
+            },
+            {
+              isLayout: true,
+              component: 'CommonLabel',
+              children:
+                '$t("Should the emails from this mailbox be imported as an archive or as regular emails?")',
+            },
+            {
+              isLayout: true,
+              element: 'ul',
+              attrs: {
+                class:
+                  'text-sm dark:text-neutral-400 text-gray-100 gap-1 list-disc ltr:ml-5 rtl:mr-5',
+              },
+              children: [
+                {
+                  isLayout: true,
+                  element: 'li',
+                  attrs: {
+                    innerHTML: markup(
+                      i18n.t(
+                        'Import as archive: |No notifications are sent|, the |tickets are closed|, and original timestamps are used. You can still find them in Zammad using the search.',
+                      ),
+                    ),
+                  },
+                  children: '',
+                },
+                {
+                  isLayout: true,
+                  element: 'li',
+                  attrs: {
+                    innerHTML: markup(
+                      i18n.t(
+                        'Import as regular: |Notifications are sent| and the |tickets are open| - you can find the tickets in the overview of open tickets.',
+                      ),
+                    ),
+                  },
+                  children: '',
+                },
+              ],
+            },
+            {
+              if: '$metaInformationInbound.archivePossible === true',
+              name: 'importAs',
+              label: __('Import as'),
+              type: 'select',
+              value: 'false',
+              options: [
+                {
+                  value: 'true',
+                  label: __('archive'),
+                },
+                {
+                  value: 'false',
+                  label: __('regular'),
+                },
+              ],
+            },
+          ],
+        },
+      ],
+    },
+  ]
+
+  const emailInboundMessageSchemaData = reactive({
+    metaInformationInbound,
+  })
+
+  return {
+    formEmailInboundMessages,
+    emailInboundMessageSchema,
+    emailInboundMessageSchemaData,
+  }
+}

+ 146 - 0
app/frontend/apps/desktop/entities/channel-email/composables/useEmailOutboundForm.ts

@@ -0,0 +1,146 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import type { ShallowRef } from 'vue'
+import { shallowRef, reactive } from 'vue'
+
+import type {
+  FormFieldValue,
+  FormRef,
+  FormSchemaField,
+} from '#shared/components/Form/types.ts'
+import { useForm } from '#shared/components/Form/useForm.ts'
+
+import type { EmailOutboundData } from '../types/email-inbound-outbound.ts'
+
+export const useEmailOutboundForm = () => {
+  const formEmailOutbound: ShallowRef<FormRef | undefined> = shallowRef()
+
+  const { updateFieldValues, values, formSetErrors } =
+    useForm<EmailOutboundData>(formEmailOutbound)
+
+  const emailOutboundFormChangeFields = reactive<
+    Record<string, Partial<FormSchemaField>>
+  >({
+    sslVerify: {},
+  })
+
+  const emailOutboundFormOnChanged = (
+    fieldName: string,
+    newValue: FormFieldValue,
+  ) => {
+    if (fieldName === 'port') {
+      const disabled = Boolean(
+        newValue && !(newValue === '465' || newValue === '587'),
+      )
+
+      emailOutboundFormChangeFields.sslVerify = {
+        disabled,
+      }
+
+      updateFieldValues({
+        sslVerify: !disabled,
+      })
+    }
+  }
+
+  const emailOutboundSchema = [
+    {
+      isLayout: true,
+      element: 'div',
+      attrs: {
+        class: 'grid grid-cols-2 gap-y-2.5 gap-x-3',
+      },
+      children: [
+        {
+          type: 'group',
+          name: 'outbound',
+          isGroupOrList: true,
+          children: [
+            {
+              name: 'adapter',
+              label: __('Send Mails via'),
+              type: 'select',
+              outerClass: 'col-span-2',
+              required: true,
+            },
+            {
+              if: '$values.adapter === "smtp"',
+              isLayout: true,
+              element: 'div',
+              attrs: {
+                class: 'grid grid-cols-2 gap-y-2.5 gap-x-3 col-span-2',
+              },
+              children: [
+                {
+                  name: 'host',
+                  label: __('Host'),
+                  type: 'text',
+                  outerClass: 'col-span-2',
+                  props: {
+                    maxLength: 120,
+                  },
+                  required: true,
+                },
+                {
+                  name: 'user',
+                  label: __('User'),
+                  type: 'text',
+                  outerClass: 'col-span-2',
+                  props: {
+                    maxLength: 120,
+                  },
+                  required: true,
+                },
+                {
+                  name: 'password',
+                  label: __('Password'),
+                  type: 'password',
+                  outerClass: 'col-span-2',
+                  props: {
+                    maxLength: 120,
+                  },
+                  required: true,
+                },
+                {
+                  name: 'port',
+                  label: __('Port'),
+                  type: 'text',
+                  outerClass: 'col-span-1',
+                  validation: 'number',
+                  props: {
+                    maxLength: 6,
+                  },
+                  required: true,
+                },
+                {
+                  name: 'sslVerify',
+                  label: __('SSL verification'),
+                  type: 'toggle',
+                  outerClass: 'col-span-1',
+                  wrapperClass: 'mt-6',
+                  value: true,
+                  props: {
+                    variants: {
+                      true: 'yes',
+                      false: 'no',
+                    },
+                  },
+                },
+              ],
+            },
+          ],
+        },
+      ],
+    },
+  ]
+
+  return {
+    formEmailOutbound,
+    emailOutboundSchema,
+    emailOutboundFormOnChanged,
+    emailOutboundFormChangeFields,
+    updateEmailOutboundFieldValues: updateFieldValues,
+    formEmailOutboundSetErrors: formSetErrors,
+    formEmailOutboundValues: values,
+  }
+}

Some files were not shown because too many files changed in this diff