From be8280c1b1e82a753a7088b9468905b0cffd492b Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sun, 16 Nov 2014 23:45:57 +0100 Subject: [PATCH] New version of setup wizard. --- .../app/controllers/getting_started.js.coffee | 692 ++++++++++----- .../app/views/getting_started/admin.jst.eco | 23 +- .../app/views/getting_started/agent.jst.eco | 25 +- .../app/views/getting_started/base.jst.eco | 59 +- .../app/views/getting_started/channel.jst.eco | 25 + .../app/views/getting_started/email.jst.eco | 82 ++ .../app/views/getting_started/finish.jst.eco | 13 + .../app/views/getting_started/index.jst.eco | 18 +- app/assets/stylesheets/zammad.css.scss | 8 +- app/controllers/getting_started_controller.rb | 821 ++++++++++++++---- app/models/channel/pop3.rb | 9 +- config/routes/getting_started.rb | 10 +- 12 files changed, 1312 insertions(+), 473 deletions(-) create mode 100644 app/assets/javascripts/app/views/getting_started/channel.jst.eco create mode 100644 app/assets/javascripts/app/views/getting_started/email.jst.eco create mode 100644 app/assets/javascripts/app/views/getting_started/finish.jst.eco diff --git a/app/assets/javascripts/app/controllers/getting_started.js.coffee b/app/assets/javascripts/app/controllers/getting_started.js.coffee index 562362f31..0a70cf205 100644 --- a/app/assets/javascripts/app/controllers/getting_started.js.coffee +++ b/app/assets/javascripts/app/controllers/getting_started.js.coffee @@ -13,7 +13,7 @@ class Index extends App.ControllerContent # if not import backend exists, go ahead if !App.Config.get('ImportPlugins') - @navigate 'getting_started/base' + @navigate 'getting_started/admin' return @fetch() @@ -51,230 +51,10 @@ class Index extends App.ControllerContent App.Config.set( 'getting_started', Index, 'Routes' ) - -class Base extends App.ControllerContent - className: 'getstarted fit' - events: - 'change [name=adapter]': 'toggleAdapter' - 'submit .base': 'storeUrl' - 'submit .base-outbound': 'storeOutbound' - 'submit .base-inbound': 'storeInbound' - 'click .js-next': 'submit' - - constructor: -> - super - - if @authenticate(true) - @navigate '#' - return - - # set title - @title 'Configure Base' - - @fetch() - - release: => - @el.removeClass('fit getstarted') - - fetch: -> - - # get data - @ajax( - id: 'getting_started', - type: 'GET', - url: @apiPath + '/getting_started', - processData: true, - success: (data, status, xhr) => - - # redirect to login if master user already exists - if @Config.get('system_init_done') - @navigate '#login' - return - - # check if import is active - if data.import_mode == true - @navigate '#import/' + data.import_backend - return - - # render page - @render() - ) - - render: -> - - @html App.view('getting_started/base')() - - # url - url = window.location.origin - configureAttributesBase = [ - { name: 'url', display: 'System URL (where the system can be reached)', tag: 'input', null: false, placeholder: 'http://yourhost', default: url }, - ] - new App.ControllerForm( - el: @$('.base-url'), - model: { configure_attributes: configureAttributesBase, className: '' }, - ) - - # outbound - adapters = - sendmail: 'Local MTA (Sendmail/Postfix/Exim/...)' - smtp: 'SMTP' - adapter_used = 'sendmail' - configureAttributesOutbound = [ - { name: 'adapter', display: 'Send Mails via', tag: 'select', multiple: false, null: false, options: adapters , default: adapter_used }, - ] - new App.ControllerForm( - el: @$('.base-outbound-type'), - model: { configure_attributes: configureAttributesOutbound, className: '' }, - ) - - @toggleAdapter() - - # inbound - configureAttributesInbound = [ - { name: 'email', display: 'Email', tag: 'input', type: 'text', limit: 200, null: false, autocapitalize: false, default: '' }, - { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: { IMAP: 'IMAP', POP3: 'POP3' } }, - { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, - { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, - { name: 'options::password', display: 'Password', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, - { name: 'options::ssl', display: 'SSL', tag: 'select', multiple: false, null: false, options: { true: 'yes', false: 'no' }, translate: true, default: true}, - ] - new App.ControllerForm( - el: @$('.base-inbound-settings'), - model: { configure_attributes: configureAttributesInbound, className: '' }, - ) - - toggleAdapter: (channel_used = {}) => - adapter = @$('[name=adapter]').val() - if adapter is 'smtp' - configureAttributesOutbound = [ - { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['host']) }, - { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['user']) }, - { name: 'options::password', display: 'Password', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['password']) }, - { name: 'options::ssl', display: 'SSL', tag: 'select', multiple: false, null: false, options: { true: 'yes', false: 'no' } , translate: true, default: (channel_used['options']&&channel_used['options']['ssl']||true) }, - { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 5, null: false, class: 'span1', autocapitalize: false, default: ((channel_used['options']&&channel_used['options']['port']) || 25) }, - ] - @form = new App.ControllerForm( - el: @$('.base-outbound-settings') - model: { configure_attributes: configureAttributesOutbound, className: '' } - autofocus: true - ) - else - @el.find('.base-outbound-settings').html('') - - submit: (e) => - e.preventDefault() - form = $(e.target).attr('data-form') - console.log('submit', form) - @$(".#{form}").trigger('submit') - - showOutbound: => - @$('.base').addClass('hide') - @$('.base-outbound').removeClass('hide') - @$('.base-inbound').addClass('hide') - @$('.wizard-controls .btn').text('Check').attr('data-form', 'base-outbound').addClass('btn--primary').removeClass('btn--danger btn--success') - @enable( @$('.btn') ) - - showInbound: => - @$('.base').addClass('hide') - @$('.base-outbound').addClass('hide') - @$('.base-inbound').removeClass('hide') - @$('.wizard-controls .btn').text('Check').attr('data-form', 'base-inbound').addClass('btn--primary').removeClass('btn--danger btn--success') - @enable( @$('.btn') ) - - storeUrl: (e) => - e.preventDefault() - - # get params - params = @formParam(e.target) - console.log('submit', params, e) - @disable(e) - - @ajax( - id: 'base_url' - type: 'POST' - url: @apiPath + '/getting_started/base_url' - data: JSON.stringify( {url:params.url} ) - processData: true - success: (data, status, xhr) => - if data.result is 'ok' - @$('.wizard-controls .btn').text('Done').removeClass('btn--primary btn--danger').addClass('btn--success') - @delay( @showOutbound, 1500 ) - else - @$('.wizard-controls .btn').text( data.message_human || data.message ).addClass('btn--danger').removeClass('btn--primary btn--success') - @enable(e) - fail: => - @enable(e) - ) - - storeOutbound: (e) => - e.preventDefault() - - # get params - params = @formParam(e.target) - @disable(e) - - @ajax( - id: 'base_outbound' - type: 'POST' - url: @apiPath + '/getting_started/base_outbound' - data: JSON.stringify( params ) - processData: true - success: (data, status, xhr) => - if data.result is 'ok' - @$('.wizard-controls .btn').text('Done').removeClass('btn--primary btn--danger').addClass('btn--success') - @delay( @showInbound, 1500 ) - else - @$('.wizard-controls .btn').text( data.message_human || data.message ).addClass('btn--danger').removeClass('btn--primary btn--success') - @enable(e) - fail: => - @enable(e) - ) - - storeInbound: (e) => - e.preventDefault() - - # get params - params = @formParam(e.target) - @disable(e) - - console.log('PA', params) - - @ajax( - id: 'base_inbound' - type: 'POST' - url: @apiPath + '/getting_started/base_inbound' - data: JSON.stringify( params ) - processData: true - success: (data, status, xhr) => - - if data.result is 'ok' - @$('.wizard-controls .btn').text('Done').removeClass('btn--primary btn--danger').addClass('btn--success') - @delay( @goToAdmin, 1500 ) - else - @$('.wizard-controls .btn').text( data.message_human || data.message ).addClass('btn--danger').removeClass('btn--primary btn--success') - @enable(e) - fail: => - @enable(e) - ) - - disable: (e) => - @formDisable(e) - @$('.wizard-controls .btn').attr('disabled', true) - - enable: (e) => - @formEnable(e) - @$('.wizard-controls .btn').attr('disabled', false) - - goToAdmin: => - @navigate 'getting_started/admin' - -App.Config.set( 'getting_started/base', Base, 'Routes' ) - - class Admin extends App.ControllerContent className: 'getstarted fit' events: - 'submit .js-admin': 'submit' + 'submit form': 'submit' constructor: -> super @@ -295,13 +75,10 @@ class Admin extends App.ControllerContent # get data @ajax( - id: 'getting_started', - type: 'GET', - url: @apiPath + '/getting_started', - data: { -# view: @view, - } - processData: true, + id: 'getting_started' + type: 'GET' + url: @apiPath + '/getting_started' + processData: true success: (data, status, xhr) => # redirect to login if master user already exists @@ -384,29 +161,35 @@ class Admin extends App.ControllerContent relogin: (data, status, xhr) => @log 'notice', 'relogin:success', data - # add notify App.Event.trigger 'notify:removeall' - @navigate 'getting_started/agents' + @navigate 'getting_started/base' App.Config.set( 'getting_started/admin', Admin, 'Routes' ) -class Agent extends App.ControllerContent - className: 'getstarted' + +class Base extends App.ControllerContent + className: 'getstarted fit' + elements: + '.logo-preview': 'logoPreview' + events: - 'submit .js-agent': 'submit' + 'submit form': 'submit' + 'change .js-upload': 'onLogoPick' constructor: -> super - return if !@authenticate() + # redirect if we are not admin + if !@authenticate(true) + @navigate '#' + return # set title - @title 'Invite Agents' + @title 'Configure Base' @fetch() - release: => @el.removeClass('fit getstarted') @@ -420,11 +203,409 @@ class Agent extends App.ControllerContent processData: true, success: (data, status, xhr) => - # redirect to login if master user already exists - if !data.setup_done - @navigate '#getting_started/admin' + # check if import is active + if data.import_mode == true + @navigate '#import/' + data.import_backend return + # render page + @render() + ) + + render: -> + + fqdn = App.Config.get('fqdn') + http_type = App.Config.get('http_type') + if !fqdn || fqdn is 'zammad.example.com' + url = window.location.origin + else + url = "#{http_type}://#{fqdn}" + + organization = App.Config.get('organization') + @html App.view('getting_started/base')( + url: url + organization: organization + ) + + onLogoPick: (event) => + reader = new FileReader() + + reader.onload = (e) => + @logoPreview.attr('src', e.target.result) + + file = event.target.files[0] + + @hideAlerts() + + # if no file is given, about in file upload was used + if !file + return + + maxSiteInMb = 3 + if file.size && file.size > 1024 * 1024 * maxSiteInMb + @showAlert( 'logo', App.i18n.translateInline( 'File too big, max. %s MB allowed.', maxSiteInMb ) ) + @logoPreview.attr( 'src', '' ) + return + + reader.readAsDataURL(file) + + submit: (e) => + e.preventDefault() + + # get params + params = @formParam(e.target) + params['logo'] = @logoPreview.attr('src') + + @hideAlerts() + @disable(e) + + @ajax( + id: 'getting_started_base' + type: 'POST' + url: @apiPath + '/getting_started/base' + data: JSON.stringify( params ) + processData: true + success: (data, status, xhr) => + if data.result is 'ok' + for key, value of data.settings + App.Config.set( key, value ) + @navigate 'getting_started/channel' + else + for key, value of data.messages + @showAlert( key, value ) + @enable(e) + fail: => + @enable(e) + ) + + hideAlerts: => + @$('.form-group').removeClass('has-error') + @$('.alert').addClass('hide') + + showAlert: (field, message) => + @$("[name=#{field}]").closest('.form-group').addClass('has-error') + @$("[name=#{field}]").closest('.form-group').find('.alert').removeClass('hide').text( App.i18n.translateInline( message ) ) + + disable: (e) => + @formDisable(e) + @$('.wizard-controls .btn').attr('disabled', true) + + enable: (e) => + @formEnable(e) + @$('.wizard-controls .btn').attr('disabled', false) + +App.Config.set( 'getting_started/base', Base, 'Routes' ) + +class Channel extends App.ControllerContent + className: 'getstarted fit' + + constructor: -> + super + + # redirect if we are not admin + if !@authenticate(true) + @navigate '#' + return + + # set title + @title 'Connect Channels' + + @adapters = [ + { + name: 'Email' + class: 'email' + link: '#getting_started/channel/email' + }, + ] + + @fetch() + + release: => + @el.removeClass('fit getstarted') + + fetch: -> + + # get data + @ajax( + id: 'getting_started', + type: 'GET', + url: @apiPath + '/getting_started', + processData: true, + success: (data, status, xhr) => + + # check if import is active + if data.import_mode == true + @navigate '#import/' + data.import_backend + return + + # render page + @render() + ) + + render: -> + @html App.view('getting_started/channel')( + adapters: @adapters + ) + +App.Config.set( 'getting_started/channel', Channel, 'Routes' ) + + +class ChannelEmail extends App.ControllerContent + className: 'getstarted fit' + events: + 'submit .js-intro': 'emailProbe' + 'submit .js-inbound': 'storeInbound' + 'change .js-outbound [name=adapter]': 'toggleAdapter' + 'submit .js-outbound': 'storeOutbound' + + constructor: -> + super + + # redirect if we are not admin + if !@authenticate(true) + @navigate '#' + return + + # set title + @title 'Email Account' + + # store account settings + @account = + inbound: {} + outbound: {} + meta: {} + + @fetch() + + release: => + @el.removeClass('fit getstarted') + + fetch: -> + + # get data + @ajax( + id: 'getting_started', + type: 'GET', + url: @apiPath + '/getting_started', + processData: true, + success: (data, status, xhr) => + + # check if import is active + if data.import_mode == true + @navigate '#import/' + data.import_backend + return + + # render page + @render() + ) + + render: -> + + @html App.view('getting_started/email')() + + # outbound + adapters = + sendmail: 'Local MTA (Sendmail/Postfix/Exim/...) - use server setup' + smtp: 'SMTP - configure your own outgoing SMTP settings' + adapter_used = 'sendmail' + configureAttributesOutbound = [ + { name: 'adapter', display: 'Send Mails via', tag: 'select', multiple: false, null: false, options: adapters , default: adapter_used }, + ] + new App.ControllerForm( + el: @$('.base-outbound-type'), + model: { configure_attributes: configureAttributesOutbound, className: '' }, + ) + @toggleAdapter() + + # inbound + configureAttributesInbound = [ + { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: { imap: 'IMAP', pop3: 'POP3' } }, + { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, + { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, + { name: 'options::password', display: 'Password', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, + { name: 'options::ssl', display: 'SSL', tag: 'select', multiple: false, null: false, options: { true: 'yes', false: 'no' }, translate: true, default: true}, + ] + new App.ControllerForm( + el: @$('.base-inbound-settings'), + model: { configure_attributes: configureAttributesInbound, className: '' }, + ) + + toggleAdapter: (channel_used = {}) => + adapter = @$('.js-outbound [name=adapter]').val() + if adapter is 'smtp' + configureAttributesOutbound = [ + { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['host']) }, + { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['user']) }, + { name: 'options::password', display: 'Password', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['password']) }, + { name: 'options::ssl', display: 'SSL', tag: 'select', multiple: false, null: false, options: { true: 'yes', false: 'no' } , translate: true, default: (channel_used['options']&&channel_used['options']['ssl']||true) }, + { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 5, null: false, class: 'span1', autocapitalize: false, default: ((channel_used['options']&&channel_used['options']['port']) || 25) }, + ] + @form = new App.ControllerForm( + el: @$('.base-outbound-settings') + model: { configure_attributes: configureAttributesOutbound, className: '' } + ) + else + @el.find('.base-outbound-settings').html('') + + emailProbe: (e) => + e.preventDefault() + params = @formParam(e.target) + + # remember account settings + @account.meta = params + + @disable(e) + @$('.js-probe .js-email').text( params.email ) + @showSlide('js-probe') + + @ajax( + id: 'email_probe' + type: 'POST' + url: @apiPath + '/getting_started/email_probe' + data: JSON.stringify( params ) + processData: true + success: (data, status, xhr) => + if data.result is 'ok' + if data.setting + for key, value of data.setting + @account[key] = value + @verify(@account) + else + @showSlide('js-inbound') + @enable(e) + fail: => + @enable(e) + @showSlide('js-intro') + ) + + showSlide: (name) => + @$('.setup.wizard').addClass('hide') + @$(".setup.wizard.#{name}").removeClass('hide') + + storeOutbound: (e) => + e.preventDefault() + + # get params + params = @formParam(e.target) + params['email'] = @account['meta']['email'] + @disable(e) + + @hideAlert('js-outbound') + + @ajax( + id: 'email_outbound' + type: 'POST' + url: @apiPath + '/getting_started/email_outbound' + data: JSON.stringify( params ) + processData: true + success: (data, status, xhr) => + if data.result is 'ok' + + # remember account settings + @account.outbound = params + + @verify(@account) + else + @showAlert('js-outbound', data.message_human || data.message ) + @enable(e) + fail: => + @enable(e) + ) + + storeInbound: (e) => + e.preventDefault() + + # get params + params = @formParam(e.target) + @disable(e) + + @hideAlert('js-inbound') + + @ajax( + id: 'email_inbound' + type: 'POST' + url: @apiPath + '/getting_started/email_inbound' + data: JSON.stringify( params ) + processData: true + success: (data, status, xhr) => + if data.result is 'ok' + @showSlide('js-outbound') + + # remember account settings + @account.inbound = params + else + @showAlert('js-inbound', data.message_human || data.message ) + @enable(e) + fail: => + @enable(e) + ) + + verify: (account) => + @showSlide('js-verify') + + @hideAlert('js-verify') + + @ajax( + id: 'email_verify' + type: 'POST' + url: @apiPath + '/getting_started/email_verify' + data: JSON.stringify( account ) + processData: true + success: (data, status, xhr) => + if data.result is 'ok' + @navigate 'getting_started/agents' + else + @showAlert('js-verify', data.message_human || data.message ) + @enable(e) + fail: => + @enable(e) + ) + + showAlert: (screen, message) => + @$(".#{screen}").find('.alert').removeClass('hide').text( App.i18n.translateInline( message ) ) + + hideAlert: (screen) => + @$(".#{screen}").find('.alert').addClass('hide') + + disable: (e) => + @formDisable(e) + @$('.wizard-controls .btn').attr('disabled', true) + + enable: (e) => + @formEnable(e) + @$('.wizard-controls .btn').attr('disabled', false) + +App.Config.set( 'getting_started/channel/email', ChannelEmail, 'Routes' ) + + +class Agent extends App.ControllerContent + className: 'getstarted fit' + events: + 'submit form': 'submit' + + constructor: -> + super + + return if !@authenticate() + + # set title + @title 'Invite Agents' + + @fetch() + + release: => + @el.removeClass('fit getstarted') + + fetch: -> + + # get data + @ajax( + id: 'getting_started', + type: 'GET', + url: @apiPath + '/getting_started', + processData: true, + success: (data, status, xhr) => + # check if import is active if data.import_mode == true @navigate '#import/' + data.import_backend @@ -495,4 +676,33 @@ class Agent extends App.ControllerContent } ) -App.Config.set( 'getting_started/agents', Agent, 'Routes' ) \ No newline at end of file +App.Config.set( 'getting_started/agents', Agent, 'Routes' ) + +class Channel extends App.ControllerContent + className: 'getstarted fit' + + constructor: -> + super + + return if !@authenticate() + + # set title + @title 'Setup Finished' + + @render() + + release: => + @el.removeClass('fit getstarted') + + render: -> + @html App.view('getting_started/finish')() + @delay( + => @$('.wizard-slide').addClass('hide') + 2300 + ) + @delay( + => @navigate '#' + 4300 + ) + +App.Config.set( 'getting_started/finish', Channel, 'Routes' ) \ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/admin.jst.eco b/app/assets/javascripts/app/views/getting_started/admin.jst.eco index 478f91905..246956835 100644 --- a/app/assets/javascripts/app/views/getting_started/admin.jst.eco +++ b/app/assets/javascripts/app/views/getting_started/admin.jst.eco @@ -1,14 +1,13 @@ -
-
-
    -
  1. <%- @T( 'Configure Base' ) %>
  2. -
  3. <%- @T( 'Create Admin' ) %>
  4. -
  5. <%- @T( 'Invite Agents' ) %>
  6. -
-
-
- -
+
+ +
+
+

<%- @T('Administrator Account') %>

+
+
+ <%- @T('Go Back') %> + +
-
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/agent.jst.eco b/app/assets/javascripts/app/views/getting_started/agent.jst.eco index a1beb07ed..05c62a866 100644 --- a/app/assets/javascripts/app/views/getting_started/agent.jst.eco +++ b/app/assets/javascripts/app/views/getting_started/agent.jst.eco @@ -1,16 +1,13 @@ -
-
-
    -
  1. <%- @T( 'Configure Base' ) %>
  2. -
  3. <%- @T( 'Create Admin' ) %>
  4. -
  5. <%- @T( 'Invite Agents' ) %>
  6. -
-
-
-
- -
-
+
+ +
+
+

<%- @T('Invite Colleagues') %>

+
+
+ <%- @T( 'Continue' ) %> + +
-
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/base.jst.eco b/app/assets/javascripts/app/views/getting_started/base.jst.eco index 531516cba..8338e834e 100644 --- a/app/assets/javascripts/app/views/getting_started/base.jst.eco +++ b/app/assets/javascripts/app/views/getting_started/base.jst.eco @@ -1,31 +1,32 @@ -
-
-
    -
  1. <%- @T( 'Configure Base' ) %>
  2. -
  3. <%- @T( 'Create Admin' ) %>
  4. -
  5. <%- @T( 'Invite Agents' ) %>
  6. -
- -
-
-
- - -
-

<%- @T('E-Mail Outbound') %>

-
-
-
- -
-

<%- @T('E-Mail Inbound') %>

-
-
- -
- <%- @T('Go Back') %> -
<%- @T('Next') %>
+
+ +
+
+

<%- @T('Organization') %>

+
+
+
+ + + +
+
+ + + +
<%- @T('Upload') %>
+
+
+ + + +

The URL to this installation of Zammad.

+
+
+
+
+ +
- -
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/channel.jst.eco b/app/assets/javascripts/app/views/getting_started/channel.jst.eco new file mode 100644 index 000000000..415307a9b --- /dev/null +++ b/app/assets/javascripts/app/views/getting_started/channel.jst.eco @@ -0,0 +1,25 @@ +
+ +
+
+

<%- @T('Connect Channels') %>

+
+

<%- @T('Setup the communication channels you want to use with your Zammad.') %>

+
+ + <% for adapter in @adapters: %> + + <% end %> + +
+
+ +
+
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/email.jst.eco b/app/assets/javascripts/app/views/getting_started/email.jst.eco new file mode 100644 index 000000000..6ee900cba --- /dev/null +++ b/app/assets/javascripts/app/views/getting_started/email.jst.eco @@ -0,0 +1,82 @@ +
+ + +
+
+

<%- @T('Email Account') %>

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ <%- @T('Go Back') %> + +
+
+
+ +
+
+

<%- @T('Email Account') %>

+
+

+ <%- @T('Testing') %> +

+
+
+
+ +
+
+

<%- @T('Email Account') %>

+
+ +

+ <%- @T('Verify sending and receiving') %> +

+
+
+
+ +
+
+

<%- @T('E-Mail Inbound') %>

+
+ +
+
+ <%- @T('Go Back') %> + +
+
+
+
+ +
+
+

<%- @T('E-Mail Outbound') %>

+
+ +
+
+
+ <%- @T('Go Back') %> + +
+
+
+
+ +
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/finish.jst.eco b/app/assets/javascripts/app/views/getting_started/finish.jst.eco new file mode 100644 index 000000000..75ded8da1 --- /dev/null +++ b/app/assets/javascripts/app/views/getting_started/finish.jst.eco @@ -0,0 +1,13 @@ +
+ +
+
+

<%- @T('Setup Finished') %>

+
+

+ Starting Zammad +

+
+
+
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/getting_started/index.jst.eco b/app/assets/javascripts/app/views/getting_started/index.jst.eco index 2a16f2aed..c7ca42bae 100644 --- a/app/assets/javascripts/app/views/getting_started/index.jst.eco +++ b/app/assets/javascripts/app/views/getting_started/index.jst.eco @@ -1,12 +1,12 @@ -
-
-
-

<%- @T('Welcome to %s', @C('product_name') ) %>

-
- +
+ + diff --git a/app/assets/stylesheets/zammad.css.scss b/app/assets/stylesheets/zammad.css.scss index 30adb6732..91d6fda84 100644 --- a/app/assets/stylesheets/zammad.css.scss +++ b/app/assets/stylesheets/zammad.css.scss @@ -4203,12 +4203,12 @@ label + .wizard-buttonList { display: block; background: hsl(0,0%,95%); width: 100%; - height: 130px; + height: 60px; margin-bottom: 10px; content: ""; color: hsl(0,0%,60%); @extend .centered; - + &:after { content: attr(data-placeholder); } @@ -4216,8 +4216,8 @@ label + .wizard-buttonList { .setup.wizard .logo-preview:not([src=""]) { content: initial; - max-width: 100%; - max-height: 130px; + max-width: 240px; + max-height: 60px; margin: 0 auto 15px; background: none; width: auto; diff --git a/app/controllers/getting_started_controller.rb b/app/controllers/getting_started_controller.rb index 54ab174bf..fe54cef7b 100644 --- a/app/controllers/getting_started_controller.rb +++ b/app/controllers/getting_started_controller.rb @@ -1,11 +1,13 @@ # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ +require 'resolv' + class GettingStartedController < ApplicationController =begin Resource: -GET /api/v1/getting_started.json +GET /api/v1/getting_started Response: { @@ -23,7 +25,7 @@ Response: } Test: -curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} +curl http://localhost/api/v1/getting_started -v -u #{login}:#{password} =end @@ -49,45 +51,634 @@ curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} } end - def base_url - return if setup_done_response + def base - # validate + # check admin permissions + return if deny_if_not_role('Admin') + + # validate url + messages = {} if !params[:url] ||params[:url] !~ /^(http|https):\/\/.+?$/ + messages[:url] = 'A URL looks like http://zammad.example.com' + end + + # validate organization + if !params[:organization] || params[:organization].empty? + messages[:organization] = 'Invalid!' + end + + # validate image + if params[:logo] && !params[:logo].empty? + content_type = nil + content = nil + + # data:image/png;base64 + if params[:logo] =~ /^data:(.+?);base64,(.+?)$/ + content_type = $1 + content = $2 + end + + if !content_type || !content + messages[:logo] = 'Unable to process image upload.' + end + end + + if !messages.empty? render :json => { - :result => 'invalid', - :message => 'Invalid!', + :result => 'invalid', + :messages => messages, } return end # split url in http_type and fqdn + settings = {} if params[:url] =~ /^(http|https):\/\/(.+?)$/ Setting.set('http_type', $1) + settings[:http_type] = $1 Setting.set('fqdn', $2) + settings[:fqdn] = $2 + end + # save organization + Setting.set('organization', params[:organization]) + settings[:organization] = params[:organization] + + # save image + if params[:logo] && !params[:logo].empty? + content_type = nil + content = nil + + # data:image/png;base64 + if params[:logo] =~ /^data:(.+?);base64,(.+?)$/ + content_type = $1 + content = Base64.decode64($2) + end + Store.remove( :object => 'System::Logo', :o_id => 1 ) + Store.add( + :object => 'System::Logo', + :o_id => 1, + :data => content, + :filename => 'image', + :preferences => { + 'Content-Type' => content_type + }, +# :created_by_id => self.updated_by_id, + ) + end + + render :json => { + :result => 'ok', + :settings => settings, + } + end + + def email_probe + + # check admin permissions + return if deny_if_not_role('Admin') + + # validation + user = nil + domain = nil + if params[:email] =~ /^(.+?)@(.+?)$/ + user = $1 + domain = $2 + end + + if !user || !domain render :json => { - :result => 'ok', + :result => 'invalid', + :messages => { + :email => 'Invalid email.' + }, + } + return + end + + # check domain based attributes + providerMap = { + :google => { + :domain => 'gmail.com|googlemail.com|gmail.de', + :inbound => { + :adapter => 'imap', + :options => { + :host => 'imap.gmail.com', + :port => '993', + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + :outbound => { + :adapter => 'smtp', + :options => { + :host => 'smtp.gmail.com', + :port => '465', + :ssl => true, + :user => params[:email], + :password => params[:password], + } + }, + }, + } + + # probe based on email domain and mx + domains = [domain] + mail_exchangers = mxers(domain) + if mail_exchangers && mail_exchangers[0] + puts "MX #{mail_exchangers} - #{mail_exchangers[0][0]}" + end + if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0] + domains.push mail_exchangers[0][0] + end + providerMap.each {|provider, settings| + domains.each {|domain_to_check| + if domain_to_check =~ /#{settings[:domain]}/i + + # probe inbound + result = email_probe_inbound( settings[:inbound] ) + if !result + render :json => result + return + end + + # probe outbound + result = email_probe_outbound( settings[:outbound], params[:email] ) + if result[:result] != 'ok' + render :json => result + return + end + + render :json => { + :result => 'ok', + :account => settings, + } + return + end + } + } + + # probe inbound + inboundMap = [] + if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0] + inboundMx = [ + { + :adapter => 'imap', + :options => { + :host => mail_exchangers[0][0], + :port => 993, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'imap', + :options => { + :host => mail_exchangers[0][0], + :port => 993, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + ] + inboundMap = inboundMap + inboundMx + end + inboundAuto = [ + { + :adapter => 'imap', + :options => { + :host => "mail.#{domain}", + :port => 993, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'imap', + :options => { + :host => "mail.#{domain}", + :port => 993, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'imap', + :options => { + :host => "imap.#{domain}", + :port => 993, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'imap', + :options => { + :host => "imap.#{domain}", + :port => 993, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'pop3', + :options => { + :host => "mail.#{domain}", + :port => 995, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'pop3', + :options => { + :host => "mail.#{domain}", + :port => 995, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'pop3', + :options => { + :host => "pop.#{domain}", + :port => 995, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'pop3', + :options => { + :host => "pop.#{domain}", + :port => 995, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'pop3', + :options => { + :host => "pop3.#{domain}", + :port => 995, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'pop3', + :options => { + :host => "pop3.#{domain}", + :port => 995, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + ] + inboundMap = inboundMap + inboundAuto + settings = {} + success = false + inboundMap.each {|config| + puts "PROBE: #{config.inspect}" + result = email_probe_inbound( config ) + puts "RESULT: #{result.inspect}" + if !result + success = true + settings[:inbound] = config + break + end + } + + if !success + render :json => { + :result => 'failed', + } + return + end + + # probe outbound + outboundMap = [] + if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0] + outboundMx = [ + { + :adapter => 'smtp', + :options => { + :host => mail_exchangers[0][0], + :port => 25, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => mail_exchangers[0][0], + :port => 25, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => mail_exchangers[0][0], + :port => 465, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => mail_exchangers[0][0], + :port => 465, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + ] + outboundMap = outboundMap + outboundMx + end + outboundAuto = [ + { + :adapter => 'smtp', + :options => { + :host => "mail.#{domain}", + :port => 25, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "mail.#{domain}", + :port => 25, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "mail.#{domain}", + :port => 465, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "mail.#{domain}", + :port => 465, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "smtp.#{domain}", + :port => 25, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "smtp.#{domain}", + :port => 25, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "smtp.#{domain}", + :port => 465, + :ssl => true, + :user => user, + :password => params[:password], + }, + }, + { + :adapter => 'smtp', + :options => { + :host => "smtp.#{domain}", + :port => 465, + :ssl => true, + :user => params[:email], + :password => params[:password], + }, + }, + ] + + success = false + outboundMap.each {|config| + puts "PROBE: #{config.inspect}" + result = email_probe_outbound( config, params[:email] ) + puts "RESULT: #{result.inspect}" + if result[:result] == 'ok' + success = true + settings[:outbound] = config + break + end + } + + if !success + render :json => { + :result => 'failed', } return end render :json => { - :result => 'invalid', - :message => 'Unable to parse url!', + :result => 'ok', + :setting => settings, } end - def base_outbound - return if setup_done_response + def email_outbound + + # check admin permissions + return if deny_if_not_role('Admin') # validate params if !params[:adapter] render :json => { + :result => 'invalid', + } + return + end + + # connection test + result = email_probe_outbound( params, params[:email] ) + if result[:result] != 'ok' + render :json => result + return + end + + # return result + render :json => { + :result => 'ok', + } + end + + def email_inbound + + # check admin permissions + return if deny_if_not_role('Admin') + + # validate params + if !params[:adapter] + render :json => { + :result => 'invalid', + } + return + end + + # connection test + result = email_probe_inbound( params ) + if result + render :json => result + return + end + + render :json => { + :result => 'ok', + } + end + + def email_verify + + # check admin permissions + return if deny_if_not_role('Admin') + + # send verify email to inbox + subject = '#' + rand(99999999999).to_s + Channel::EmailSend.new.send( + { + :from => params[:meta][:email], + :to => params[:meta][:email], + :subject => "Zammad Getting started Test Email #{subject}", + :body => '.', + 'x-zammad-ignore' => 'true', + } + ) + (1..5).each {|loop| + sleep 10 + + # fetch mailbox + found = nil + if params[:inbound][:adapter] =~ /^imap$/i + found = Channel::IMAP.new.fetch( { :options => params[:inbound][:options] }, 'verify', subject ) + else + found = Channel::POP3.new.fetch( { :options => params[:inbound][:options] }, 'verify', subject ) + end + + if found && found == 'verify ok' + + # remember address + address = EmailAddress.all.first + if address + address.update_attributes( + :realname => params[:meta][:realname], + :email => params[:meta][:email], + :active => 1, + :updated_by_id => 1, + :created_by_id => 1, + ) + else + EmailAddress.create( + :realname => params[:meta][:realname], + :email => params[:meta][:email], + :active => 1, + :updated_by_id => 1, + :created_by_id => 1, + ) + end + + # store mailbox + Channel.create( + :area => 'Email::Inbound', + :adapter => params[:inbound][:adapter], + :options => params[:inbound][:options], + :group_id => 1, + :active => 1, + :updated_by_id => 1, + :created_by_id => 1, + ) + + # save settings + if params[:outbound][:adapter] =~ /^smtp$/i + smtp = Channel.where( :adapter => 'SMTP', :area => 'Email::Outbound' ).first + smtp.options = params[:outbound][:options] + smtp.active = true + smtp.save! + sendmail = Channel.where( :adapter => 'Sendmail' ).first + sendmail.active = false + sendmail.save! + else + sendmail = Channel.where( :adapter => 'Sendmail', :area => 'Email::Outbound' ).first + sendmail.options = {} + sendmail.active = true + sendmail.save! + smtp = Channel.where( :adapter => 'SMTP' ).first + smtp.active = false + smtp.save + end + + render :json => { + :result => 'ok', + } + return + end + } + + # check dilivery for 30 sek. + render :json => { + :result => 'invalid', + :message => 'Verification Email not found in mailbox.', + } + end + + private + + def email_probe_outbound(params, email) + + # validate params + if !params[:adapter] + result = { :result => 'invalid', :message => 'Invalid!', } - return + return result end # test connection @@ -101,7 +692,7 @@ curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} begin Channel::SMTP.new.send( { - :from => 'me@example.com', + :from => email, :to => 'emailtrytest@znuny.com', :subject => 'test', :body => 'test', @@ -118,10 +709,10 @@ curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} } whiteMap.each {|key, message| if e.message =~ /#{Regexp.escape(key)}/i - render :json => { + result = { :result => 'ok', } - return + return result end } message_human = '' @@ -130,75 +721,48 @@ curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} message_human = message end } - render :json => { + result = { :result => 'invalid', :message => e.message, :message_human => message_human, } - return - end - - else - begin - Channel::Sendmail.new.send( - { - :from => 'me@example.com', - :to => 'emailtrytest@znuny.com', - :subject => 'test', - :body => 'test', - }, - nil - ) - rescue Exception => e - message_human = '' - translationMap.each {|key, message| - if e.message =~ /#{Regexp.escape(key)}/i - message_human = message - end - } - render :json => { - :result => 'invalid', - :message => e.message, - :message_human => message_human, - } - return + return result end + return end - # save settings - if params[:adapter] == 'smtp' - smtp = Channel.where( :adapter => 'SMTP', :area => 'Email::Outbound' ).first - smtp.options = params[:options] - smtp.active = true - smtp.save! - sendmail = Channel.where(:adapter => 'Sendmail').first - sendmail.active = false - sendmail.save! - else - sendmail = Channel.where( :adapter => 'Sendmail', :area => 'Email::Outbound' ).first - sendmail.options = {} - sendmail.active = true - sendmail.save! - smtp = Channel.where(:adapter => 'SMTP').first - smtp.active = false - smtp.save + begin + Channel::Sendmail.new.send( + { + :from => email, + :to => 'emailtrytest@znuny.com', + :subject => 'test', + :body => 'test', + }, + nil + ) + rescue Exception => e + message_human = '' + translationMap.each {|key, message| + if e.message =~ /#{Regexp.escape(key)}/i + message_human = message + end + } + result = { + :result => 'invalid', + :message => e.message, + :message_human => message_human, + } + return result end - - # return result - render :json => { - :result => 'ok', - } + return end - def base_inbound - return if setup_done_response + def email_probe_inbound(params) # validate params if !params[:adapter] - render :json => { - :result => 'invalid', - } - return + raise 'need :adapter param' end # connection test @@ -208,7 +772,7 @@ curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} 'No route to host' => 'No route to host!', 'Connection refused' => 'Connection refused!', } - if params[:adapter] == 'IMAP' + if params[:adapter] =~ /^imap$/i begin Channel::IMAP.new.fetch( { :options => params[:options] }, 'check' ) rescue Exception => e @@ -218,103 +782,42 @@ curl http://localhost/api/v1/getting_started.json -v -u #{login}:#{password} message_human = message end } - render :json => { + result = { :result => 'invalid', :message => e.message, :message_human => message_human, } - return - end - else - begin - Channel::POP3.new.fetch( { :options => params[:options] }, 'check' ) - rescue Exception => e - message_human = '' - translationMap.each {|key, message| - if e.message =~ /#{Regexp.escape(key)}/i - message_human = message - end - } - render :json => { - :result => 'invalid', - :message => e.message, - :message_human => message_human, - } - return + return result end + return end - # send verify email to inbox - subject = '#' + rand(99999999999).to_s - Channel::EmailSend.new.send( - { - :from => params[:email], - :to => params[:email], - :subject => "Zammad Getting started Test Email #{subject}", - :body => '.', - 'x-zammad-ignore' => 'true', - } - ) - (1..5).each {|loop| - sleep 10 - - # fetch mailbox - found = nil - if params[:adapter] == 'IMAP' - found = Channel::IMAP.new.fetch( { :options => params[:options] }, 'verify', subject ) - else - found = Channel::POP3.new.fetch( { :options => params[:options] }, 'verify', subject ) - end - - if found && found == 'verify ok' - - # remember address - address = EmailAddress.all.first - if address - address.update_attributes( - :realname => 'Zammad', - :email => params[:email], - :active => 1, - :updated_by_id => 1, - :created_by_id => 1, - ) - else - EmailAddress.create( - :realname => 'Zammad', - :email => params[:email], - :active => 1, - :updated_by_id => 1, - :created_by_id => 1, - ) + begin + Channel::POP3.new.fetch( { :options => params[:options] }, 'check' ) + rescue Exception => e + message_human = '' + translationMap.each {|key, message| + if e.message =~ /#{Regexp.escape(key)}/i + message_human = message end - - # store mailbox - Channel.create( - :area => 'Email::Inbound', - :adapter => params[:adapter], - :options => params[:options], - :group_id => 1, - :active => 1, - :updated_by_id => 1, - :created_by_id => 1, - ) - - render :json => { - :result => 'ok', - } - return - end - } - - # check dilivery for 30 sek. - render :json => { - :result => 'invalid', - :message => 'Verification Email not found in mailbox.', - } + } + result = { + :result => 'invalid', + :message => e.message, + :message_human => message_human, + } + return result + end return end - private + def mxers(domain) + mxs = Resolv::DNS.open do |dns| + ress = dns.getresources(domain, Resolv::DNS::Resource::IN::MX) + ress.map { |r| [r.exchange.to_s, IPSocket::getaddress(r.exchange.to_s), r.preference] } + end + mxs + end def setup_done #return false diff --git a/app/models/channel/pop3.rb b/app/models/channel/pop3.rb index 9d6a47641..7c2e92a33 100644 --- a/app/models/channel/pop3.rb +++ b/app/models/channel/pop3.rb @@ -15,8 +15,15 @@ class Channel::POP3 < Channel::EmailParser puts "fetching pop3 (#{channel[:options][:host]}/#{channel[:options][:user]} port=#{port},ssl=#{ssl})" @pop = Net::POP3.new( channel[:options][:host], port ) + + # on check, reduce open_timeout to have faster probing + if check_type == 'check' + @pop.open_timeout = 5 + @pop.read_timeout = 6 + end + if ssl - @pop.enable_ssl + @pop.enable_ssl(OpenSSL::SSL::VERIFY_NONE) end @pop.start( channel[:options][:user], channel[:options][:password] ) if check_type == 'check' diff --git a/config/routes/getting_started.rb b/config/routes/getting_started.rb index 505f5a15e..b12ff6481 100644 --- a/config/routes/getting_started.rb +++ b/config/routes/getting_started.rb @@ -2,9 +2,11 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # getting_started - match api_path + '/getting_started', :to => 'getting_started#index', :via => :get - match api_path + '/getting_started/base_url', :to => 'getting_started#base_url', :via => :post - match api_path + '/getting_started/base_outbound', :to => 'getting_started#base_outbound', :via => :post - match api_path + '/getting_started/base_inbound', :to => 'getting_started#base_inbound', :via => :post + match api_path + '/getting_started', :to => 'getting_started#index', :via => :get + match api_path + '/getting_started/base', :to => 'getting_started#base', :via => :post + match api_path + '/getting_started/email_probe', :to => 'getting_started#email_probe', :via => :post + match api_path + '/getting_started/email_outbound', :to => 'getting_started#email_outbound', :via => :post + match api_path + '/getting_started/email_inbound', :to => 'getting_started#email_inbound', :via => :post + match api_path + '/getting_started/email_verify', :to => 'getting_started#email_verify', :via => :post end \ No newline at end of file