useEmailChannelConfiguration.ts 11 KB

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