useEmailChannelConfiguration.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { computed, ref, watch } from 'vue'
  3. import type { FormSubmitData } from '#shared/components/Form/types.ts'
  4. import { useDebouncedLoading } from '#shared/composables/useDebouncedLoading.ts'
  5. import UserError from '#shared/errors/UserError.ts'
  6. import type {
  7. ChannelEmailInboundConfiguration,
  8. ChannelEmailOutboundConfiguration,
  9. } from '#shared/graphql/types.ts'
  10. import { i18n } from '#shared/i18n.ts'
  11. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  12. import type { MutationSendError } from '#shared/types/error.ts'
  13. import { useChannelEmailAddMutation } from '#desktop/entities/channel-email/graphql/mutations/channelEmailAdd.api.ts'
  14. import { useChannelEmailValidateConfigurationRoundtripMutation } from '#desktop/entities/channel-email/graphql/mutations/channelEmailValidateConfigurationRoundtrip.api.ts'
  15. import { useChannelEmailGuessConfigurationMutation } from '../graphql/mutations/channelEmailGuessConfiguration.api.ts'
  16. import { useChannelEmailValidateConfigurationInboundMutation } from '../graphql/mutations/channelEmailValidateConfigurationInbound.api.ts'
  17. import { useChannelEmailValidateConfigurationOutboundMutation } from '../graphql/mutations/channelEmailValidateConfigurationOutbound.api.ts'
  18. import type { EmailAccountData } from '../types/email-account.ts'
  19. import type {
  20. EmailChannelSteps,
  21. EmailChannelForms,
  22. } from '../types/email-channel.ts'
  23. import type {
  24. UpdateMetaInformationInboundFunction,
  25. EmailInboundMetaInformation,
  26. EmailOutboundData,
  27. EmailInboundData,
  28. EmailInboundMessagesData,
  29. } from '../types/email-inbound-outbound.ts'
  30. import type { SetNonNullable, SetOptional } from 'type-fest'
  31. import type { Ref } from 'vue'
  32. export const useEmailChannelConfiguration = (
  33. emailChannelForms: EmailChannelForms,
  34. metaInformationInbound: Ref<Maybe<EmailInboundMetaInformation>>,
  35. updateMetaInformationInbound: UpdateMetaInformationInboundFunction,
  36. onSuccessCallback: () => void,
  37. ) => {
  38. const { loading, debouncedLoading } = useDebouncedLoading()
  39. const activeStep = ref<EmailChannelSteps>('account')
  40. const pendingActiveStep = ref<Maybe<EmailChannelSteps>>(null)
  41. const setActiveStep = (nextStep: EmailChannelSteps) => {
  42. if (!debouncedLoading.value) {
  43. activeStep.value = nextStep
  44. return
  45. }
  46. pendingActiveStep.value = nextStep
  47. }
  48. watch(debouncedLoading, (newValue: boolean) => {
  49. if (!newValue && pendingActiveStep.value) {
  50. activeStep.value = pendingActiveStep.value
  51. pendingActiveStep.value = null
  52. }
  53. })
  54. const stepTitle = computed(() => {
  55. switch (activeStep.value) {
  56. case 'inbound':
  57. return __('Email Inbound')
  58. case 'inbound-messages':
  59. return __('Archive Emails')
  60. case 'outbound':
  61. return __('Email Outbound')
  62. default:
  63. return __('Email Account')
  64. }
  65. })
  66. const activeForm = computed(() => {
  67. switch (activeStep.value) {
  68. case 'inbound':
  69. return emailChannelForms.emailInbound.form.value
  70. case 'inbound-messages':
  71. return emailChannelForms.emailInboundMessages.form.value
  72. case 'outbound':
  73. return emailChannelForms.emailOutbound.form.value
  74. default:
  75. return emailChannelForms.emailAccount.form.value
  76. }
  77. })
  78. const validateConfigurationRoundtripAndChannelAdd = async (
  79. account: EmailAccountData,
  80. inboundConfiguration: EmailInboundData,
  81. outboundConfiguration: EmailOutboundData,
  82. ) => {
  83. const validateConfigurationRoundtripMutation = new MutationHandler(
  84. useChannelEmailValidateConfigurationRoundtripMutation(),
  85. )
  86. const addEmailChannelMutation = new MutationHandler(
  87. useChannelEmailAddMutation(),
  88. )
  89. // Transform port field to real number for usage in the mutation.
  90. inboundConfiguration.port = Number(inboundConfiguration.port)
  91. outboundConfiguration.port = Number(outboundConfiguration.port)
  92. // Extend inbound configuration with archive information when needed.
  93. if (metaInformationInbound.value?.archive) {
  94. inboundConfiguration = {
  95. ...inboundConfiguration,
  96. archive: true,
  97. archiveBefore: metaInformationInbound.value.archiveBefore,
  98. archiveStateId: metaInformationInbound.value.archiveStateId,
  99. }
  100. }
  101. try {
  102. const roundTripResult = await validateConfigurationRoundtripMutation.send(
  103. {
  104. inboundConfiguration,
  105. outboundConfiguration,
  106. emailAddress: account.email,
  107. },
  108. )
  109. if (
  110. roundTripResult?.channelEmailValidateConfigurationRoundtrip?.success
  111. ) {
  112. try {
  113. const addChannelResult = await addEmailChannelMutation.send({
  114. input: {
  115. inboundConfiguration,
  116. outboundConfiguration,
  117. emailAddress: account.email,
  118. emailRealname: account.realname,
  119. },
  120. })
  121. if (addChannelResult?.channelEmailAdd?.channel) {
  122. onSuccessCallback()
  123. }
  124. } catch (errors) {
  125. emailChannelForms.emailAccount.setErrors(errors as MutationSendError)
  126. setActiveStep('account')
  127. }
  128. }
  129. } catch (errors) {
  130. if (
  131. errors instanceof UserError &&
  132. Object.keys(errors.getFieldErrorList()).length > 0
  133. ) {
  134. if (
  135. Object.keys(errors.getFieldErrorList()).some((key) =>
  136. key.startsWith('outbound'),
  137. )
  138. ) {
  139. setActiveStep('outbound')
  140. emailChannelForms.emailOutbound.setErrors(errors as MutationSendError)
  141. } else {
  142. setActiveStep('inbound')
  143. emailChannelForms.emailInbound.setErrors(errors as MutationSendError)
  144. }
  145. return
  146. }
  147. emailChannelForms.emailAccount.setErrors(
  148. new UserError([
  149. {
  150. message: i18n.t(
  151. 'Email sending and receiving could not be verified. Please check your settings.',
  152. ),
  153. },
  154. ]),
  155. )
  156. setActiveStep('account')
  157. }
  158. }
  159. const guessEmailAccount = (data: FormSubmitData<EmailAccountData>) => {
  160. loading.value = true
  161. const guessConfigurationMutation = new MutationHandler(
  162. useChannelEmailGuessConfigurationMutation(),
  163. )
  164. return guessConfigurationMutation
  165. .send({
  166. emailAddress: data.email,
  167. password: data.password,
  168. })
  169. .then(async (result) => {
  170. if (
  171. result?.channelEmailGuessConfiguration?.result.inboundConfiguration &&
  172. result?.channelEmailGuessConfiguration?.result.outboundConfiguration
  173. ) {
  174. const inboundConfiguration = result.channelEmailGuessConfiguration
  175. .result.inboundConfiguration as SetOptional<
  176. SetNonNullable<Required<ChannelEmailInboundConfiguration>>,
  177. '__typename'
  178. >
  179. delete inboundConfiguration.__typename
  180. const outboundConfiguration = result.channelEmailGuessConfiguration
  181. .result.outboundConfiguration as SetOptional<
  182. SetNonNullable<Required<ChannelEmailOutboundConfiguration>>,
  183. '__typename'
  184. >
  185. delete outboundConfiguration.__typename
  186. emailChannelForms.emailInbound.updateFieldValues(inboundConfiguration)
  187. emailChannelForms.emailOutbound.updateFieldValues(
  188. outboundConfiguration,
  189. )
  190. const mailboxStats =
  191. result?.channelEmailGuessConfiguration?.result.mailboxStats
  192. if (
  193. mailboxStats?.contentMessages &&
  194. mailboxStats?.contentMessages > 0
  195. ) {
  196. updateMetaInformationInbound(mailboxStats, 'roundtrip')
  197. setActiveStep('inbound-messages')
  198. return
  199. }
  200. await validateConfigurationRoundtripAndChannelAdd(
  201. data,
  202. inboundConfiguration,
  203. outboundConfiguration,
  204. )
  205. } else {
  206. emailChannelForms.emailInbound.updateFieldValues({
  207. user: data.email,
  208. password: data.password,
  209. })
  210. emailChannelForms.emailOutbound.updateFieldValues({
  211. user: data.email,
  212. password: data.password,
  213. })
  214. emailChannelForms.emailInbound.setErrors(
  215. new UserError([
  216. {
  217. message: i18n.t(
  218. 'The server settings could not be automatically detected. Please configure them manually.',
  219. ),
  220. },
  221. ]),
  222. )
  223. setActiveStep('inbound')
  224. }
  225. })
  226. .finally(() => {
  227. loading.value = false
  228. })
  229. }
  230. const validateEmailInbound = (data: FormSubmitData<EmailInboundData>) => {
  231. loading.value = true
  232. const validationConfigurationInbound = new MutationHandler(
  233. useChannelEmailValidateConfigurationInboundMutation(),
  234. )
  235. return validationConfigurationInbound
  236. .send({
  237. inboundConfiguration: {
  238. ...data,
  239. port: Number(data.port),
  240. },
  241. })
  242. .then((result) => {
  243. if (result?.channelEmailValidateConfigurationInbound?.success) {
  244. emailChannelForms.emailOutbound.updateFieldValues({
  245. host: data.host,
  246. user: data.user,
  247. password: data.password,
  248. })
  249. const mailboxStats =
  250. result?.channelEmailValidateConfigurationInbound?.mailboxStats
  251. if (
  252. mailboxStats?.contentMessages &&
  253. mailboxStats?.contentMessages > 0
  254. ) {
  255. updateMetaInformationInbound(mailboxStats, 'outbound')
  256. setActiveStep('inbound-messages')
  257. return
  258. }
  259. setActiveStep('outbound')
  260. }
  261. })
  262. .finally(() => {
  263. loading.value = false
  264. })
  265. }
  266. const importEmailInboundMessages = async (
  267. data: FormSubmitData<EmailInboundMessagesData>,
  268. ) => {
  269. if (metaInformationInbound.value && data.archive) {
  270. metaInformationInbound.value.archive = true
  271. metaInformationInbound.value.archiveBefore = data.archive_before as string
  272. metaInformationInbound.value.archiveStateId =
  273. data.archive_state_id as number
  274. }
  275. if (metaInformationInbound.value?.nextAction === 'outbound') {
  276. setActiveStep('outbound')
  277. }
  278. if (metaInformationInbound.value?.nextAction === 'roundtrip') {
  279. loading.value = true
  280. await validateConfigurationRoundtripAndChannelAdd(
  281. emailChannelForms.emailAccount.values.value,
  282. emailChannelForms.emailInbound.values.value,
  283. emailChannelForms.emailOutbound.values.value,
  284. )
  285. loading.value = false
  286. }
  287. }
  288. const validateEmailOutbound = (data: FormSubmitData<EmailOutboundData>) => {
  289. loading.value = true
  290. const validationConfigurationOutbound = new MutationHandler(
  291. useChannelEmailValidateConfigurationOutboundMutation(),
  292. )
  293. return validationConfigurationOutbound
  294. .send({
  295. outboundConfiguration: {
  296. ...data,
  297. port: Number(data.port),
  298. },
  299. emailAddress: emailChannelForms.emailAccount.values.value
  300. ?.email as string,
  301. })
  302. .then(async (result) => {
  303. if (result?.channelEmailValidateConfigurationOutbound?.success) {
  304. await validateConfigurationRoundtripAndChannelAdd(
  305. emailChannelForms.emailAccount.values.value,
  306. emailChannelForms.emailInbound.values.value,
  307. emailChannelForms.emailOutbound.values.value,
  308. )
  309. }
  310. })
  311. .finally(() => {
  312. loading.value = false
  313. })
  314. }
  315. return {
  316. debouncedLoading,
  317. stepTitle,
  318. activeStep,
  319. activeForm,
  320. guessEmailAccount,
  321. validateEmailInbound,
  322. importEmailInboundMessages,
  323. validateEmailOutbound,
  324. }
  325. }