microsoft365.coffee 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. class App.ChannelMicrosoft365 extends App.ControllerTabs
  2. @requiredPermission: 'admin.channel_microsoft365'
  3. header: __('Microsoft 365 IMAP Email')
  4. constructor: ->
  5. super
  6. @title __('Microsoft 365 IMAP Email'), true
  7. @tabs = [
  8. {
  9. name: __('Accounts'),
  10. target: 'c-account',
  11. controller: ChannelAccountOverview,
  12. },
  13. {
  14. name: __('Filter'),
  15. target: 'c-filter',
  16. controller: App.ChannelEmailFilter,
  17. },
  18. {
  19. name: __('Signatures'),
  20. target: 'c-signature',
  21. controller: App.ChannelEmailSignature,
  22. },
  23. {
  24. name: __('Settings'),
  25. target: 'c-setting',
  26. controller: App.SettingsArea,
  27. params: { area: 'Email::Base' },
  28. },
  29. ]
  30. @render()
  31. class ChannelAccountOverview extends App.ControllerSubContent
  32. @requiredPermission: 'admin.channel_microsoft365'
  33. events:
  34. 'click .js-new': 'new'
  35. 'click .js-admin-consent': 'adminConsent'
  36. 'click .js-delete': 'delete'
  37. 'click .js-reauthenticate': 'reauthenticate'
  38. 'click .js-configApp': 'configApp'
  39. 'click .js-disable': 'disable'
  40. 'click .js-enable': 'enable'
  41. 'click .js-channelGroupChange': 'groupChange'
  42. 'click .js-editInbound': 'editInbound'
  43. 'click .js-rollbackMigration': 'rollbackMigration'
  44. 'click .js-emailAddressNew': 'emailAddressNew'
  45. 'click .js-emailAddressEdit': 'emailAddressEdit'
  46. 'click .js-emailAddressDelete': 'emailAddressDelete',
  47. constructor: ->
  48. super
  49. @interval(@load, 30000)
  50. @load()
  51. load: (reset_channel_id = false) =>
  52. if reset_channel_id
  53. @channel_id = undefined
  54. @navigate '#channels/microsoft365'
  55. @startLoading()
  56. @ajax(
  57. id: 'microsoft365_index'
  58. type: 'GET'
  59. url: "#{@apiPath}/channels_microsoft365"
  60. processData: true
  61. success: (data, status, xhr) =>
  62. @stopLoading()
  63. App.Collection.loadAssets(data.assets)
  64. @callbackUrl = data.callback_url
  65. @render(data)
  66. )
  67. render: (data) =>
  68. # if no microsoft365 app is registered, show intro
  69. external_credential = App.ExternalCredential.findByAttribute('name', 'microsoft365')
  70. if !external_credential
  71. @html App.view('microsoft365/index')()
  72. if @channel_id
  73. @configApp()
  74. return
  75. channels = []
  76. for channel_id in data.channel_ids
  77. channel = App.Channel.find(channel_id)
  78. if channel.group_id
  79. channel.group = App.Group.find(channel.group_id)
  80. else
  81. channel.group = '-'
  82. email_addresses = App.EmailAddress.search(filter: { channel_id: channel.id })
  83. channel.email_addresses = email_addresses
  84. channels.push channel
  85. # on a channel migration we need to auto redirect
  86. # the user to the "Add Account" functionality after
  87. # the filled up the external credentials
  88. if @channel_id
  89. item = App.Channel.find(@channel_id)
  90. if item && item.area != 'Microsoft365::Account'
  91. @new()
  92. return
  93. # get all unlinked email addresses
  94. not_used_email_addresses = []
  95. for email_address_id in data.not_used_email_address_ids
  96. not_used_email_addresses.push App.EmailAddress.find(email_address_id)
  97. @html App.view('microsoft365/list')(
  98. channels: channels
  99. external_credential: external_credential
  100. not_used_email_addresses: not_used_email_addresses
  101. )
  102. # on a channel creation we will auto open the edit
  103. # dialog after the redirect back to zammad to optional
  104. # change the inbound configuration, but not for
  105. # migrated channel because we guess that the inbound configuration
  106. # is already correct for them.
  107. if @channel_id
  108. item = App.Channel.find(@channel_id)
  109. if item && item.area == 'Microsoft365::Account' && item.options && item.options.backup_imap_classic is undefined && not @error_code
  110. @editInbound(undefined, @channel_id, true, true)
  111. @channel_id = undefined
  112. if @error_code is 'AADSTS65004'
  113. @error_code = undefined
  114. new App.AdminConsentInfo(container: @container, type: 'microsoft365')
  115. if @error_code is 'user_mismatch'
  116. @error_code = undefined
  117. new App.UserMismatchInfo(container: @container, type: 'microsoft365', item: item)
  118. if @error_code is 'duplicate_email_address'
  119. @error_code = undefined
  120. new App.DuplicateEmailAddressInfo(container: @container, type: 'microsoft365', emailAddress: if @param then decodeURIComponent(@param))
  121. show: (params) =>
  122. for key, value of params
  123. if key isnt 'el' && key isnt 'shown' && key isnt 'match'
  124. @[key] = value
  125. configApp: =>
  126. new AppConfig(
  127. container: @el.parents('.content')
  128. callbackUrl: @callbackUrl
  129. load: @load
  130. )
  131. new: (e) ->
  132. window.location.href = "#{@apiPath}/external_credentials/microsoft365/link_account"
  133. adminConsent: (e) ->
  134. window.location.href = "#{@apiPath}/external_credentials/microsoft365/link_account?prompt=consent"
  135. delete: (e) =>
  136. e.preventDefault()
  137. id = $(e.target).closest('.action').data('id')
  138. new App.ControllerConfirm(
  139. message: __('Are you sure?')
  140. buttonClass: 'btn--danger'
  141. callback: =>
  142. @ajax(
  143. id: 'microsoft365_delete'
  144. type: 'DELETE'
  145. url: "#{@apiPath}/channels_microsoft365"
  146. data: JSON.stringify(id: id)
  147. processData: true
  148. success: =>
  149. @load()
  150. )
  151. container: @el.closest('.content')
  152. )
  153. reauthenticate: (e) =>
  154. e.preventDefault()
  155. id = $(e.target).closest('.action').data('id')
  156. window.location.href = "#{@apiPath}/external_credentials/microsoft365/link_account?channel_id=#{id}"
  157. disable: (e) =>
  158. e.preventDefault()
  159. id = $(e.target).closest('.action').data('id')
  160. @ajax(
  161. id: 'microsoft365_disable'
  162. type: 'POST'
  163. url: "#{@apiPath}/channels_microsoft365_disable"
  164. data: JSON.stringify(id: id)
  165. processData: true
  166. success: =>
  167. @load()
  168. )
  169. enable: (e) =>
  170. e.preventDefault()
  171. id = $(e.target).closest('.action').data('id')
  172. @ajax(
  173. id: 'microsoft365_enable'
  174. type: 'POST'
  175. url: "#{@apiPath}/channels_microsoft365_enable"
  176. data: JSON.stringify(id: id)
  177. processData: true
  178. success: =>
  179. @load()
  180. )
  181. editInbound: (e, channel_id, set_active, redirect = false) =>
  182. if !channel_id
  183. e.preventDefault()
  184. channel_id = $(e.target).closest('.action').data('id')
  185. item = App.Channel.find(channel_id)
  186. new ChannelInboundEdit(
  187. container: @el.closest('.content')
  188. item: item
  189. callback: @load
  190. set_active: set_active
  191. redirect: redirect
  192. )
  193. rollbackMigration: (e) =>
  194. e.preventDefault()
  195. id = $(e.target).closest('.action').data('id')
  196. @ajax(
  197. id: 'microsoft365_rollback_migration'
  198. type: 'POST'
  199. url: "#{@apiPath}/channels_microsoft365_rollback_migration"
  200. data: JSON.stringify(id: id)
  201. processData: true
  202. success: =>
  203. @load()
  204. @notify
  205. type: 'success'
  206. msg: __('Rollback of channel migration succeeded!')
  207. error: (data) =>
  208. @notify
  209. type: 'error'
  210. msg: __('Failed to roll back the migration of the channel!')
  211. )
  212. groupChange: (e) =>
  213. e.preventDefault()
  214. id = $(e.target).closest('.action').data('id')
  215. item = App.Channel.find(id)
  216. new ChannelGroupEdit(
  217. container: @el.closest('.content')
  218. item: item
  219. callback: @load
  220. )
  221. emailAddressNew: (e) =>
  222. e.preventDefault()
  223. channel_id = $(e.target).closest('.action').data('id')
  224. new App.ControllerGenericNew(
  225. pageData:
  226. object: __('Email Address')
  227. genericObject: 'EmailAddress'
  228. container: @el.closest('.content')
  229. item:
  230. channel_id: channel_id
  231. callback: @load
  232. )
  233. emailAddressEdit: (e) =>
  234. e.preventDefault()
  235. id = $(e.target).closest('li').data('id')
  236. new App.ControllerGenericEdit(
  237. pageData:
  238. object: __('Email Address')
  239. genericObject: 'EmailAddress'
  240. container: @el.closest('.content')
  241. id: id
  242. callback: @load
  243. )
  244. emailAddressDelete: (e) =>
  245. e.preventDefault()
  246. id = $(e.target).closest('li').data('id')
  247. item = App.EmailAddress.find(id)
  248. new App.ControllerGenericDestroyConfirm(
  249. item: item
  250. container: @el.closest('.content')
  251. callback: @load
  252. )
  253. class ChannelInboundEdit extends App.ControllerModal
  254. buttonClose: true
  255. buttonCancel: true
  256. buttonSubmit: true
  257. head: __('Channel')
  258. content: =>
  259. configureAttributesBase = [
  260. { name: 'options::folder', display: __('Folder'), tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false },
  261. { name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
  262. ]
  263. @form = new App.ControllerForm(
  264. model:
  265. configure_attributes: configureAttributesBase
  266. className: ''
  267. params: @item.options.inbound
  268. )
  269. @form.form
  270. onSubmit: (e) =>
  271. @startLoading()
  272. # get params
  273. params = @formParam(e.target)
  274. # validate form
  275. errors = @form.validate(params)
  276. # show errors in form
  277. if errors
  278. @log 'error', errors
  279. @formValidate(form: e.target, errors: errors)
  280. return false
  281. # disable form
  282. @formDisable(e)
  283. # probe
  284. @ajax(
  285. id: 'channel_email_inbound'
  286. type: 'POST'
  287. url: "#{@apiPath}/channels_microsoft365_inbound/#{@item.id}"
  288. data: JSON.stringify(params)
  289. processData: true
  290. success: (data, status, xhr) =>
  291. if data.content_messages or not @set_active
  292. new App.ChannelInboundEmailArchive(
  293. container: @el.closest('.content')
  294. item: @item
  295. content_messages: data.content_messages
  296. inboundParams: params
  297. callback: @verify
  298. )
  299. @close()
  300. return
  301. @verify(params)
  302. error: (xhr) =>
  303. data = JSON.parse(xhr.responseText)
  304. @stopLoading()
  305. @formEnable(e)
  306. @el.find('.alert--danger').removeClass('hide').text(data.error_human || data.error || __('The changes could not be saved.'))
  307. )
  308. verify: (params = {}) =>
  309. @startLoading()
  310. if @set_active
  311. params['active'] = true
  312. # update
  313. @ajax(
  314. id: 'channel_email_verify'
  315. type: 'POST'
  316. url: "#{@apiPath}/channels_microsoft365_verify/#{@item.id}"
  317. data: JSON.stringify(params)
  318. processData: true
  319. success: (data, status, xhr) =>
  320. @callback(true)
  321. @close()
  322. error: (xhr) =>
  323. data = JSON.parse(xhr.responseText)
  324. @stopLoading()
  325. @el.find('.alert--danger').removeClass('hide').text(data.error_human || data.error || __('The changes could not be saved.'))
  326. )
  327. onCancel: =>
  328. return if not @redirect
  329. @navigate '#channels/microsoft365'
  330. class ChannelGroupEdit extends App.ControllerModal
  331. buttonClose: true
  332. buttonCancel: true
  333. buttonSubmit: true
  334. head: __('Channel')
  335. content: =>
  336. configureAttributesBase = [
  337. { name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
  338. ]
  339. @form = new App.ControllerForm(
  340. model:
  341. configure_attributes: configureAttributesBase
  342. className: ''
  343. params: @item
  344. )
  345. @form.form
  346. onSubmit: (e) =>
  347. # get params
  348. params = @formParam(e.target)
  349. # validate form
  350. errors = @form.validate(params)
  351. # show errors in form
  352. if errors
  353. @log 'error', errors
  354. @formValidate(form: e.target, errors: errors)
  355. return false
  356. # disable form
  357. @formDisable(e)
  358. # update
  359. @ajax(
  360. id: 'channel_email_group'
  361. type: 'POST'
  362. url: "#{@apiPath}/channels_microsoft365_group/#{@item.id}"
  363. data: JSON.stringify(params)
  364. processData: true
  365. success: (data, status, xhr) =>
  366. @callback()
  367. @close()
  368. error: (xhr) =>
  369. data = JSON.parse(xhr.responseText)
  370. @formEnable(e)
  371. @el.find('.alert--danger').removeClass('hide').text(data.error || __('The changes could not be saved.'))
  372. )
  373. class AppConfig extends App.ControllerModal
  374. head: __('Connect Microsoft 365 App')
  375. shown: true
  376. button: 'Connect'
  377. buttonCancel: true
  378. small: true
  379. content: ->
  380. @external_credential = App.ExternalCredential.findByAttribute('name', 'microsoft365')
  381. content = $(App.view('microsoft365/app_config')(
  382. external_credential: @external_credential
  383. callbackUrl: @callbackUrl
  384. ))
  385. content.find('.js-select').on('click', (e) =>
  386. @selectAll(e)
  387. )
  388. content
  389. onClosed: =>
  390. return if !@isChanged
  391. @isChanged = false
  392. @load()
  393. onSubmit: (e) =>
  394. @formDisable(e)
  395. # verify app credentials
  396. @ajax(
  397. id: 'microsoft365_app_verify'
  398. type: 'POST'
  399. url: "#{@apiPath}/external_credentials/microsoft365/app_verify"
  400. data: JSON.stringify(@formParams())
  401. processData: true
  402. success: (data, status, xhr) =>
  403. if data.attributes
  404. if !@external_credential
  405. @external_credential = new App.ExternalCredential
  406. @external_credential.load(name: 'microsoft365', credentials: data.attributes)
  407. @external_credential.save(
  408. done: =>
  409. @isChanged = true
  410. @close()
  411. fail: =>
  412. @el.find('.alert--danger').removeClass('hide').text(__('The entry could not be created.'))
  413. )
  414. return
  415. @formEnable(e)
  416. @el.find('.alert--danger').removeClass('hide').text(data.error || __('App could not be verified.'))
  417. )
  418. class App.AdminConsentInfo extends App.ControllerModal
  419. buttonClose: true
  420. small: true
  421. buttonSubmit: __('Close')
  422. head: __('Admin Consent')
  423. content: ->
  424. App.view('microsoft365/admin_consent')()
  425. onSubmit: =>
  426. @close()
  427. onClosed: =>
  428. return if not @type
  429. @navigate "#channels/#{@type}"
  430. class App.UserMismatchInfo extends App.ControllerModal
  431. buttonClose: true
  432. small: true
  433. buttonSubmit: __('Close')
  434. head: __('User Mismatch')
  435. content: ->
  436. App.view('microsoft365/user_mismatch')(item: @item)
  437. onSubmit: =>
  438. @close()
  439. onClosed: =>
  440. return if not @type
  441. @navigate "#channels/#{@type}"
  442. class App.DuplicateEmailAddressInfo extends App.ControllerModal
  443. buttonClose: true
  444. small: true
  445. buttonSubmit: __('Close')
  446. head: __('Duplicate Email Address')
  447. content: ->
  448. App.view('microsoft365/duplicate_email_address')(emailAddress: @emailAddress)
  449. onSubmit: =>
  450. @close()
  451. onClosed: =>
  452. return if not @type
  453. @navigate "#channels/#{@type}"
  454. App.Config.set('microsoft365', { prio: 5000, name: __('Microsoft 365 IMAP Email'), parent: '#channels', target: '#channels/microsoft365', controller: App.ChannelMicrosoft365, permission: ['admin.channel_microsoft365'] }, 'NavBarAdmin')