<%- @T('You can connect %s with Zammad. You need to connect your Zammad with %s first.', 'Office 365 Accounts', 'Office 365') %>
+diff --git a/.rubocop/todo.rspec.yml b/.rubocop/todo.rspec.yml index 237eb4ca5..d4081517d 100644 --- a/.rubocop/todo.rspec.yml +++ b/.rubocop/todo.rspec.yml @@ -42,6 +42,7 @@ RSpec/ContextWording: - 'spec/lib/auth_spec.rb' - 'spec/lib/core_ext/string_spec.rb' - 'spec/lib/external_credential/google_spec.rb' + - 'spec/lib/external_credential/office365_spec.rb' - 'spec/lib/html_sanitizer_spec.rb' - 'spec/lib/import/exchange/folder_spec.rb' - 'spec/lib/import/helper_spec.rb' @@ -144,6 +145,7 @@ RSpec/ContextWording: - 'spec/requests/external_credentials_spec.rb' - 'spec/requests/integration/check_mk_spec.rb' - 'spec/requests/integration/gmail_spec.rb' + - 'spec/requests/integration/office365_spec.rb' - 'spec/requests/integration/object_manager_attributes_spec.rb' - 'spec/requests/integration/smime_spec.rb' - 'spec/requests/knowledge_base/attachments_spec.rb' @@ -176,6 +178,7 @@ RSpec/ExampleLength: - 'spec/lib/auto_wizard_spec.rb' - 'spec/lib/core_ext/string_spec.rb' - 'spec/lib/external_credential/google_spec.rb' + - 'spec/lib/external_credential/office365_spec.rb' - 'spec/lib/external_sync_spec.rb' - 'spec/lib/import/import_job_backend_examples.rb' - 'spec/lib/import/ldap_spec.rb' @@ -356,6 +359,7 @@ RSpec/LetSetup: Exclude: - 'spec/jobs/ticket_online_notification_seen_job_spec.rb' - 'spec/lib/external_credential/google_spec.rb' + - 'spec/lib/external_credential/office365_spec.rb' - 'spec/lib/secure_mailing/smime_spec.rb' - 'spec/lib/sessions/backend/ticket_overview_list_spec.rb' - 'spec/models/channel/driver/twitter_spec.rb' @@ -615,6 +619,7 @@ RSpec/NestedGroups: - 'spec/lib/core_ext/string_spec.rb' - 'spec/lib/email_address_validation_spec.rb' - 'spec/lib/external_credential/google_spec.rb' + - 'spec/lib/external_credential/office365_spec.rb' - 'spec/lib/html_sanitizer_spec.rb' - 'spec/lib/import/exchange/folder_spec.rb' - 'spec/lib/notification_factory/mailer_spec.rb' diff --git a/.rubocop/todo.yml b/.rubocop/todo.yml index 59659c0fb..8815b42a8 100644 --- a/.rubocop/todo.yml +++ b/.rubocop/todo.yml @@ -26,6 +26,7 @@ Metrics/AbcSize: - 'app/controllers/attachments_controller.rb' - 'app/controllers/channels_email_controller.rb' - 'app/controllers/channels_google_controller.rb' + - 'app/controllers/channels_office365_controller.rb' - 'app/controllers/channels_sms_controller.rb' - 'app/controllers/channels_telegram_controller.rb' - 'app/controllers/channels_twitter_controller.rb' @@ -273,6 +274,7 @@ Metrics/AbcSize: - 'lib/excel_sheet/ticket.rb' - 'lib/external_credential/facebook.rb' - 'lib/external_credential/google.rb' + - 'lib/external_credential/office365.rb' - 'lib/external_credential/twitter.rb' - 'lib/facebook.rb' - 'lib/fill_db.rb' @@ -456,6 +458,7 @@ Metrics/CyclomaticComplexity: - 'app/controllers/application_controller/renders_models.rb' - 'app/controllers/channels_email_controller.rb' - 'app/controllers/channels_google_controller.rb' + - 'app/controllers/channels_office365_controller.rb' - 'app/controllers/channels_twitter_controller.rb' - 'app/controllers/concerns/checks_user_attributes_by_current_user_permission.rb' - 'app/controllers/concerns/creates_ticket_articles.rb' @@ -613,6 +616,7 @@ Metrics/CyclomaticComplexity: - 'lib/excel_sheet.rb' - 'lib/external_credential/facebook.rb' - 'lib/external_credential/google.rb' + - 'lib/external_credential/office365.rb' - 'lib/external_credential/twitter.rb' - 'lib/facebook.rb' - 'lib/fill_db.rb' @@ -693,6 +697,7 @@ Metrics/PerceivedComplexity: - 'app/controllers/application_controller/logs_http_access.rb' - 'app/controllers/channels_email_controller.rb' - 'app/controllers/channels_google_controller.rb' + - 'app/controllers/channels_office365_controller.rb' - 'app/controllers/concerns/creates_ticket_articles.rb' - 'app/controllers/first_steps_controller.rb' - 'app/controllers/form_controller.rb' @@ -837,6 +842,7 @@ Metrics/PerceivedComplexity: - 'lib/excel_sheet.rb' - 'lib/external_credential/facebook.rb' - 'lib/external_credential/google.rb' + - 'lib/external_credential/office365.rb' - 'lib/external_credential/twitter.rb' - 'lib/facebook.rb' - 'lib/fill_db.rb' @@ -934,6 +940,7 @@ Style/OptionalBooleanParameter: - 'lib/core_ext/string.rb' - 'lib/external_credential/facebook.rb' - 'lib/external_credential/google.rb' + - 'lib/external_credential/office365.rb' - 'lib/external_credential/twitter.rb' - 'lib/html_sanitizer.rb' - 'lib/models.rb' diff --git a/app/assets/javascripts/app/controllers/_channel/email.coffee b/app/assets/javascripts/app/controllers/_channel/email.coffee index 74809e7e3..2c55b286c 100644 --- a/app/assets/javascripts/app/controllers/_channel/email.coffee +++ b/app/assets/javascripts/app/controllers/_channel/email.coffee @@ -188,6 +188,7 @@ class App.ChannelEmailAccountOverview extends App.Controller 'click .js-emailAddressDelete': 'emailAddressDelete', 'click .js-editNotificationOutbound': 'editNotificationOutbound' 'click .js-migrateGoogleMail': 'migrateGoogleMail' + 'click .js-migrateOffice365Mail': 'migrateOffice365Mail' constructor: -> super @@ -385,6 +386,10 @@ class App.ChannelEmailAccountOverview extends App.Controller id = $(e.target).closest('.action').data('id') @navigate "#channels/google/#{id}" + migrateOffice365Mail: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + @navigate "#channels/office365/#{id}" class App.ChannelEmailEdit extends App.ControllerModal buttonClose: true diff --git a/app/assets/javascripts/app/controllers/_channel/office365.coffee b/app/assets/javascripts/app/controllers/_channel/office365.coffee new file mode 100644 index 000000000..7fbb4b29a --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/office365.coffee @@ -0,0 +1,416 @@ +class App.ChannelOffice365 extends App.ControllerTabs + requiredPermission: 'admin.channel_office365' + header: 'Office 365' + constructor: -> + super + + @title 'Office 365', true + + @tabs = [ + { + name: 'Accounts', + target: 'c-account', + controller: ChannelOffice365AccountOverview, + }, + { + name: 'Filter', + target: 'c-filter', + controller: App.ChannelEmailFilter, + }, + { + name: 'Signatures', + target: 'c-signature', + controller: App.ChannelEmailSignature, + }, + { + name: 'Settings', + target: 'c-setting', + controller: App.SettingsArea, + params: { area: 'Email::Base' }, + }, + ] + + @render() + +class ChannelOffice365AccountOverview extends App.ControllerSubContent + requiredPermission: 'admin.channel_office365' + events: + 'click .js-new': 'new' + 'click .js-delete': 'delete' + 'click .js-configApp': 'configApp' + 'click .js-disable': 'disable' + 'click .js-enable': 'enable' + 'click .js-channelGroupChange': 'groupChange' + 'click .js-editInbound': 'editInbound' + 'click .js-rollbackMigration': 'rollbackMigration' + 'click .js-emailAddressNew': 'emailAddressNew' + 'click .js-emailAddressEdit': 'emailAddressEdit' + 'click .js-emailAddressDelete': 'emailAddressDelete', + + constructor: -> + super + + #@interval(@load, 60000) + @load() + + load: (reset_channel_id = false) => + if reset_channel_id + @channel_id = undefined + @navigate '#channels/office365' + + @startLoading() + @ajax( + id: 'office365_index' + type: 'GET' + url: "#{@apiPath}/channels_office365" + processData: true + success: (data, status, xhr) => + @stopLoading() + App.Collection.loadAssets(data.assets) + @callbackUrl = data.callback_url + @render(data) + ) + + render: (data) => + + # if no office365 app is registered, show intro + external_credential = App.ExternalCredential.findByAttribute('name', 'office365') + if !external_credential + @html App.view('office365/index')() + if @channel_id + @configApp() + return + + channels = [] + for channel_id in data.channel_ids + channel = App.Channel.find(channel_id) + if channel.group_id + channel.group = App.Group.find(channel.group_id) + else + channel.group = '-' + + email_addresses = App.EmailAddress.search(filter: { channel_id: channel.id }) + channel.email_addresses = email_addresses + + channels.push channel + + # auto redirect to gmail account linking if we have no account + if @channel_id && channels.length < 1 + @new() + return + + # get all unlinked email addresses + not_used_email_addresses = [] + for email_address_id in data.not_used_email_address_ids + not_used_email_addresses.push App.EmailAddress.find(email_address_id) + + @html App.view('office365/list')( + channels: channels + external_credential: external_credential + not_used_email_addresses: not_used_email_addresses + ) + + if @channel_id + item = App.Channel.find(@channel_id) + if item && item.options && item.options.backup_imap_classic is undefined + @editInbound(undefined, @channel_id, true) + @channel_id = undefined + + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + configApp: => + new AppConfig( + container: @el.parents('.content') + callbackUrl: @callbackUrl + load: @load + ) + + new: (e) -> + window.location.href = "#{@apiPath}/external_credentials/office365/link_account" + + delete: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + new App.ControllerConfirm( + message: 'Sure?' + callback: => + @ajax( + id: 'office365_delete' + type: 'DELETE' + url: "#{@apiPath}/channels_office365" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) + container: @el.closest('.content') + ) + + disable: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + @ajax( + id: 'office365_disable' + type: 'POST' + url: "#{@apiPath}/channels_office365_disable" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) + + enable: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + @ajax( + id: 'office365_enable' + type: 'POST' + url: "#{@apiPath}/channels_office365_enable" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + ) + + editInbound: (e, channel_id, set_active) => + if !channel_id + e.preventDefault() + channel_id = $(e.target).closest('.action').data('id') + item = App.Channel.find(channel_id) + new App.ChannelInboundEdit( + container: @el.closest('.content') + item: item + callback: @load + set_active: set_active, + ) + + rollbackMigration: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + @ajax( + id: 'office365_rollback_migration' + type: 'POST' + url: "#{@apiPath}/channels_office365_rollback_migration" + data: JSON.stringify(id: id) + processData: true + success: => + @load() + @notify + type: 'success' + msg: 'Rollback of channel migration succeeded!' + error: (data) => + @notify + type: 'error' + msg: 'Failed to rollback migration of the channel!' + ) + + groupChange: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + item = App.Channel.find(id) + new App.ChannelGroupEdit( + container: @el.closest('.content') + item: item + callback: @load + ) + + emailAddressNew: (e) => + e.preventDefault() + channel_id = $(e.target).closest('.action').data('id') + new App.ControllerGenericNew( + pageData: + object: 'Email Address' + genericObject: 'EmailAddress' + container: @el.closest('.content') + item: + channel_id: channel_id + callback: @load + ) + + emailAddressEdit: (e) => + e.preventDefault() + id = $(e.target).closest('li').data('id') + new App.ControllerGenericEdit( + pageData: + object: 'Email Address' + genericObject: 'EmailAddress' + container: @el.closest('.content') + id: id + callback: @load + ) + + emailAddressDelete: (e) => + e.preventDefault() + id = $(e.target).closest('li').data('id') + item = App.EmailAddress.find(id) + new App.ControllerGenericDestroyConfirm( + item: item + container: @el.closest('.content') + callback: @load + ) + +class App.ChannelInboundEdit extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: true + head: 'Channel' + + content: => + configureAttributesBase = [ + { name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false }, + { name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false }, + ] + @form = new App.ControllerForm( + model: + configure_attributes: configureAttributesBase + className: '' + params: @item.options.inbound + ) + @form.form + + onSubmit: (e) => + @startLoading() + + # get params + params = @formParam(e.target) + + # validate form + errors = @form.validate(params) + + # show errors in form + if errors + @log 'error', errors + @formValidate(form: e.target, errors: errors) + return false + + # disable form + @formDisable(e) + + if @set_active + params['active'] = true + + # update + @ajax( + id: 'channel_email_inbound' + type: 'POST' + url: "#{@apiPath}/channels_office365_inbound/#{@item.id}" + data: JSON.stringify(params) + processData: true + success: (data, status, xhr) => + @callback(true) + @close() + error: (xhr) => + @stopLoading() + @formEnable(e) + details = xhr.responseJSON || {} + @notify + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to save changes.') + timeout: 6000 + ) + +class App.ChannelGroupEdit extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: true + head: 'Channel' + + content: => + configureAttributesBase = [ + { name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true, filter: { active: true } }, + ] + @form = new App.ControllerForm( + model: + configure_attributes: configureAttributesBase + className: '' + params: @item + ) + @form.form + + onSubmit: (e) => + + # get params + params = @formParam(e.target) + + # validate form + errors = @form.validate(params) + + # show errors in form + if errors + @log 'error', errors + @formValidate(form: e.target, errors: errors) + return false + + # disable form + @formDisable(e) + + # update + @ajax( + id: 'channel_email_group' + type: 'POST' + url: "#{@apiPath}/channels_office365_group/#{@item.id}" + data: JSON.stringify(params) + processData: true + success: (data, status, xhr) => + @callback() + @close() + error: (xhr) => + data = JSON.parse(xhr.responseText) + @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to save changes.') + ) + +class AppConfig extends App.ControllerModal + head: 'Connect Office 365 App' + shown: true + button: 'Connect' + buttonCancel: true + small: true + + content: -> + @external_credential = App.ExternalCredential.findByAttribute('name', 'office365') + content = $(App.view('office365/app_config')( + external_credential: @external_credential + callbackUrl: @callbackUrl + )) + content.find('.js-select').on('click', (e) => + @selectAll(e) + ) + content + + onClosed: => + return if !@isChanged + @isChanged = false + @load() + + onSubmit: (e) => + @formDisable(e) + + # verify app credentials + @ajax( + id: 'office365_app_verify' + type: 'POST' + url: "#{@apiPath}/external_credentials/office365/app_verify" + data: JSON.stringify(@formParams()) + processData: true + success: (data, status, xhr) => + if data.attributes + if !@external_credential + @external_credential = new App.ExternalCredential + @external_credential.load(name: 'office365', credentials: data.attributes) + @external_credential.save( + done: => + @isChanged = true + @close() + fail: => + @el.find('.alert').removeClass('hidden').text('Unable to create entry.') + ) + return + @formEnable(e) + @el.find('.alert').removeClass('hidden').text(data.error || 'Unable to verify App.') + ) + +App.Config.set('office365', { prio: 5000, name: 'Office 365', parent: '#channels', target: '#channels/office365', controller: App.ChannelOffice365, permission: ['admin.channel_office365'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/models/email_address.coffee b/app/assets/javascripts/app/models/email_address.coffee index b8f4c2089..7409b98bd 100644 --- a/app/assets/javascripts/app/models/email_address.coffee +++ b/app/assets/javascripts/app/models/email_address.coffee @@ -18,7 +18,7 @@ class App.EmailAddress extends App.Model if localChannel return channel if channel.area is localChannel.area else - return channel if channel.area is 'Google::Account' || channel.area is 'Email::Account' + return channel if channel.area is 'Google::Account' || channel.area is 'Office365::Account' || channel.area is 'Email::Account' ) @configure_attributes = [ diff --git a/app/assets/javascripts/app/views/channel/email_account_overview.jst.eco b/app/assets/javascripts/app/views/channel/email_account_overview.jst.eco index 5936e5f95..c1fe4dc33 100644 --- a/app/assets/javascripts/app/views/channel/email_account_overview.jst.eco +++ b/app/assets/javascripts/app/views/channel/email_account_overview.jst.eco @@ -56,6 +56,19 @@ <% end %> <% end %> + <% if channel.active is true && channel.options.inbound && channel.options.inbound.options && channel.options.inbound.options.host == 'outlook.office365.com' && channel.options.outbound && channel.options.outbound.options && channel.options.outbound.options.host == 'smtp.office365.com': %> +
+ <%- @T('The tutorial on how to manage a %s is hosted on our online documentation %l.', 'Office 365 App', 'https://admin-docs.zammad.org/en/latest/channels/office365.html') %> +
+ diff --git a/app/assets/javascripts/app/views/office365/index.jst.eco b/app/assets/javascripts/app/views/office365/index.jst.eco new file mode 100644 index 000000000..105e5292e --- /dev/null +++ b/app/assets/javascripts/app/views/office365/index.jst.eco @@ -0,0 +1,12 @@ +<%- @T('You can connect %s with Zammad. You need to connect your Zammad with %s first.', 'Office 365 Accounts', 'Office 365') %>
+