email.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { uniq } from 'lodash-es'
  3. import { ref } from 'vue'
  4. import { useEmailFileUrls } from '#shared/composables/useEmailFileUrls.ts'
  5. import { getTicketSignatureQuery } from '#shared/composables/useTicketSignature.ts'
  6. import type {
  7. TicketArticle,
  8. TicketById,
  9. } from '#shared/entities/ticket/types.ts'
  10. import { EnumTicketArticleSenderName } from '#shared/graphql/types.ts'
  11. import { getIdFromGraphQLId } from '#shared/graphql/utils.ts'
  12. import { textCleanup } from '#shared/utils/helpers.ts'
  13. import openExternalLink from '#shared/utils/openExternalLink.ts'
  14. import { forwardEmail } from './email/forward.ts'
  15. import { replyToEmail } from './email/reply.ts'
  16. import type {
  17. TicketFieldsType,
  18. TicketArticleAction,
  19. TicketArticleActionPlugin,
  20. TicketArticleSelectionOptions,
  21. TicketArticleType,
  22. } from './types.ts'
  23. const canReplyAll = (article: TicketArticle) => {
  24. const addresses = [article.to, article.cc]
  25. if (article.sender?.name === EnumTicketArticleSenderName.Customer) {
  26. addresses.push(article.from)
  27. }
  28. const foreignRecipients = addresses
  29. .flatMap((address) => address?.parsed || [])
  30. .filter((address) => address.emailAddress && !address.isSystemAddress)
  31. .map((address) => address.emailAddress)
  32. return uniq(foreignRecipients).length > 1
  33. }
  34. const addSignature = async (
  35. ticket: TicketById,
  36. { body }: TicketArticleSelectionOptions,
  37. position?: number,
  38. ) => {
  39. const ticketSignature = getTicketSignatureQuery()
  40. const { data: signature } = await ticketSignature.query({
  41. variables: {
  42. groupId: ticket.group.id,
  43. ticketId: ticket.id,
  44. },
  45. })
  46. const text = signature?.ticketSignature?.renderedBody
  47. const id = signature?.ticketSignature?.id
  48. if (!text || !id) {
  49. body.removeSignature()
  50. return
  51. }
  52. body.addSignature({
  53. body: textCleanup(text),
  54. id: getIdFromGraphQLId(id),
  55. position,
  56. })
  57. }
  58. const actionPlugin: TicketArticleActionPlugin = {
  59. order: 200,
  60. addActions(ticket, article, { config }) {
  61. if (!ticket.group.emailAddress) return []
  62. const type = article.type?.name
  63. const sender = article.sender?.name
  64. const actions: TicketArticleAction[] = []
  65. const isEmail = type === 'email' || type === 'web'
  66. const isPhone =
  67. type === 'phone' &&
  68. (sender === EnumTicketArticleSenderName.Customer ||
  69. sender === EnumTicketArticleSenderName.Agent)
  70. if (isEmail || isPhone) {
  71. actions.push(
  72. {
  73. apps: ['mobile', 'desktop'],
  74. name: 'email-reply',
  75. view: { agent: ['change'] },
  76. label: __('Reply'),
  77. icon: 'reply',
  78. alwaysVisible: true,
  79. perform: (t, a, o) => replyToEmail(t, a, o, config),
  80. },
  81. {
  82. apps: ['mobile', 'desktop'],
  83. name: 'email-forward',
  84. view: { agent: ['change'] },
  85. label: __('Forward'),
  86. icon: 'forward',
  87. perform: (t, a, o) => forwardEmail(t, a, o, config),
  88. },
  89. )
  90. }
  91. if (isEmail && canReplyAll(article)) {
  92. actions.push({
  93. apps: ['mobile', 'desktop'],
  94. name: 'email-reply-all',
  95. view: { agent: ['change'] },
  96. label: __('Reply All'),
  97. icon: 'reply-alt',
  98. alwaysVisible: true,
  99. perform: (t, a, o) => replyToEmail(t, a, o, config, true),
  100. })
  101. }
  102. if (isEmail) {
  103. const emailFileUrls = useEmailFileUrls(article, ref(ticket.internalId))
  104. if (emailFileUrls.originalFormattingUrl.value) {
  105. actions.push({
  106. apps: ['desktop'],
  107. name: 'email-download-original-email',
  108. view: { agent: ['read'] },
  109. label: __('Download original email'),
  110. icon: 'download',
  111. perform: () =>
  112. openExternalLink(
  113. emailFileUrls.originalFormattingUrl.value as string,
  114. ),
  115. })
  116. }
  117. if (emailFileUrls.rawMessageUrl.value) {
  118. actions.push({
  119. apps: ['desktop'],
  120. name: 'email-download-raw-email',
  121. view: { agent: ['read'] },
  122. label: __('Download raw email'),
  123. icon: 'download',
  124. perform: () =>
  125. openExternalLink(emailFileUrls.rawMessageUrl.value as string),
  126. })
  127. }
  128. }
  129. return actions
  130. },
  131. addTypes(ticket, { config }) {
  132. if (!ticket.group.emailAddress) return []
  133. const fields: Partial<TicketFieldsType> = {
  134. to: { required: true },
  135. cc: {},
  136. subject: {},
  137. body: {
  138. required: true,
  139. },
  140. subtype: {},
  141. attachments: {},
  142. security: {},
  143. }
  144. if (!config.ui_ticket_zoom_article_email_subject) delete fields.subject
  145. const type: TicketArticleType = {
  146. value: 'email',
  147. label: __('Email'),
  148. buttonLabel: __('Add email'),
  149. apps: ['mobile', 'desktop'],
  150. icon: 'mail',
  151. view: { agent: ['change'] },
  152. fields,
  153. onDeselected(_, { body }) {
  154. getTicketSignatureQuery().cancel()
  155. body.removeSignature()
  156. },
  157. onOpened(_, { body }) {
  158. // always reset position if reply is added as a new article
  159. return addSignature(ticket, { body }, 1)
  160. },
  161. onSelected(_, { body }) {
  162. // try to dynamically set cursor position, dependeing on where it was before signature was added
  163. return addSignature(ticket, { body })
  164. },
  165. internal: false,
  166. performReply(ticket) {
  167. return {
  168. subtype: 'reply',
  169. to: ticket.customer.email ? [ticket.customer.email] : [],
  170. }
  171. },
  172. }
  173. return [type]
  174. },
  175. }
  176. export default actionPlugin