Fixes #5470 - Unify destination group setting and include e-mail-address in channel setup.

Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
This commit is contained in:
Dusan Vuckovic 2025-02-18 13:38:47 +01:00
parent 93f0ec942d
commit 340d9c1014
23 changed files with 724 additions and 368 deletions

View file

@ -187,7 +187,7 @@ class ChannelEmailAccountOverview extends App.Controller
e.preventDefault()
id = $(e.target).closest('.action').data('id')
item = App.Channel.find(id)
new ChannelEmailEdit(
new ChannelGroupEdit(
container: @el.closest('.content')
item: item
callback: @load
@ -250,8 +250,9 @@ class ChannelEmailAccountOverview extends App.Controller
id = $(e.target).closest('.action').data('id')
@navigate "#channels/microsoft365/#{id}"
class ChannelGroupEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
class ChannelEmailEdit extends App.ControllerModal
buttonClose: true
buttonCancel: true
buttonSubmit: true
@ -259,14 +260,18 @@ class ChannelEmailEdit extends App.ControllerModal
content: =>
configureAttributesBase = [
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
]
@form = new App.ControllerForm(
model:
configure_attributes: configureAttributesBase
className: ''
params: @item
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
onSubmit: (e) =>
@ -283,6 +288,8 @@ class ChannelEmailEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
@processDestinationGroupEmailAddressParams(params)
# disable form
@formDisable(e)
@ -303,6 +310,8 @@ class ChannelEmailEdit extends App.ControllerModal
)
class ChannelEmailAccountWizard extends App.ControllerWizardModal
@include App.DestinationGroupEmailAddressesMixin
elements:
'.modal-body': 'body'
events:
@ -382,8 +391,10 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
{ name: 'realname', display: __('Organization & Department Name'), tag: 'input', type: 'text', limit: 160, null: false, placeholder: __('Organization Support'), autocomplete: 'off' },
{ name: 'email', display: __('Email'), tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
{ name: 'password', display: __('Password'), tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group' },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@channel?.id, @channel?.group_id) },
]
@formMeta = new App.ControllerForm(
el: @$('.base-settings'),
model:
@ -408,6 +419,8 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
# inbound
configureAttributesInbound = [
{ name: 'group_id', display: __('Destination Group'), tag: 'select', null: false, relation: 'Group' },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@channel?.id, @channel?.group_id) },
{ name: 'adapter', display: __('Type'), tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound, translate: true },
{ 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, autocomplete: 'off' },
@ -419,15 +432,13 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
{ name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
]
# If email inbound form is opened from the new email wizard, show additional fields on top.
if !@channel
#Email Inbound form opened from new email wizard, show full settings
configureAttributesInbound = [
{ name: 'options::realname', display: __('Organization & Department Name'), tag: 'input', type: 'text', limit: 160, null: false, placeholder: __('Organization Support'), autocomplete: 'off' },
{ name: 'options::email', display: __('Email'), tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
{ name: 'options::group_id', display: __('Destination Group'), tag: 'select', null: false, relation: 'Group', nulloption: true },
].concat(configureAttributesInbound)
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
return if !params
if params.adapter is 'imap'
@ -437,23 +448,29 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
ui.hide('options::folder')
ui.hide('options::keep_on_server')
form = new App.ControllerForm(
@form = new App.ControllerForm(
el: @$('.base-inbound-settings'),
model:
configure_attributes: configureAttributesInbound
className: ''
params: @account.inbound
params: _.extend(
@account.inbound
group_id: @account?.meta?.group_id or @channel?.group_id
group_email_address_id: @account?.meta?.group_email_address_id
)
handlers: [
showHideFolder,
showHideFolder
@destinationGroupEmailAddressFormHandler(@channel)
]
)
@toggleInboundAdapter()
form.el.find("select[name='options::ssl']").off('change').on('change', (e) ->
@form.el.find("select[name='options::ssl']").off('change').on('change', (e) =>
if $(e.target).val() is 'ssl'
form.el.find("[name='options::port']").val('993')
@form.el.find("[name='options::port']").val('993')
else if $(e.target).val() is 'off'
form.el.find("[name='options::port']").val('143')
@form.el.find("[name='options::port']").val('143')
)
toggleInboundAdapter: =>
@ -537,8 +554,21 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
probeBasedOnIntro: (e) =>
e.preventDefault()
# get params
params = @formParam(e.target)
if not $(e.currentTarget).hasClass('js-expert')
# validate form
errors = @formMeta.validate(params)
# show errors in form
if errors
@log 'error', errors
@formValidate(form: e.target, errors: errors)
return false
# remember account settings
@account.meta = params
@ -552,18 +582,21 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
@$('.js-inbound [name="options::password"]').val(params.password)
@$('.js-inbound [name="options::email"]').val(params.email)
@$('.js-inbound [name="options::realname"]').val(params.realname)
@$('.js-inbound [name="options::group_id"]').val(params.group_id)
@$('.js-inbound [name="group_id"]').val(params.group_id)
@$('.js-inbound [name="group_email_address_id"]').val(params.group_email_address_id)
return
@disable(e)
@$('.js-probe .js-email').text(params.email)
@showSlide('js-probe')
data = _.pick(params, 'email', 'password')
@ajax(
id: 'email_probe'
type: 'POST'
url: "#{@apiPath}/channels_email_probe"
data: JSON.stringify(params)
data: JSON.stringify(data)
processData: true
success: (data, status, xhr) =>
if data.result is 'ok'
@ -586,7 +619,8 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
@$('.js-inbound [name="options::password"]').val(@account['meta']['password'])
@$('.js-inbound [name="options::email"]').val(@account['meta']['email'])
@$('.js-inbound [name="options::realname"]').val(@account['meta']['realname'])
@$('.js-inbound [name="options::group_id"]').val(@account['meta']['group_id'])
@$('.js-inbound [name="group_id"]').val(@account['meta']['group_id'])
@$('.js-inbound [name="group_email_address_id"]').val(@account['meta']['group_email_address_id'])
@enable(e)
error: =>
@ -600,13 +634,25 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
# 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
if params.options && params.options.password is @passwordPlaceholder
params.options.password = @inboundPassword
# Update meta as the one from AttributesBase could be outdated
@account.meta.realname = params.options.realname
@account.meta.email = params.options.email
@account.meta.group_id = params.options.group_id
@account.meta.group_id = params.group_id
@account.meta.group_email_address_id = params.group_email_address_id
delete params.group_id
delete params.group_email_address_id
# let backend know about the channel
if @channel
@ -817,11 +863,18 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal
if @channel
params.channel_id = @channel.id
if params.meta.group_id
if params.meta?.group_id
params.group_id = params.meta.group_id
else if @channel.group_id
else if @channel?.group_id
params.group_id = @channel.group_id
# Copy group email address parameter from meta key to the root.
if not _.isUndefined(params.meta?.group_email_address_id)
params.group_email_address = params.meta.group_email_address_id isnt 'false'
if params.group_email_address and params.meta.group_email_address_id isnt 'true'
params.group_email_address_id = params.meta.group_email_address_id
if !params.email && @channel
email_addresses = App.EmailAddress.search(filter: { channel_id: @channel.id })
if email_addresses && email_addresses[0]

View file

@ -269,6 +269,8 @@ class ChannelAccountOverview extends App.ControllerSubContent
)
class ChannelInboundEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
buttonClose: true
buttonCancel: true
buttonSubmit: true
@ -276,14 +278,20 @@ class ChannelInboundEdit extends App.ControllerModal
content: =>
configureAttributesBase = [
{ name: 'options::folder', display: __('Folder'), tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, placeholder: __('optional') },
{ name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@item.id, @item.group_id) },
{ name: 'options::folder', display: __('Folder'), tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, placeholder: __('optional') },
{ 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
params: _.extend(
@item.options.inbound
group_id: @item.group_id
)
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
@ -302,6 +310,9 @@ class ChannelInboundEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
data =
options: params.options
# disable form
@formDisable(e)
@ -310,7 +321,7 @@ class ChannelInboundEdit extends App.ControllerModal
id: 'channel_email_inbound'
type: 'POST'
url: "#{@apiPath}/channels_google_inbound/#{@item.id}"
data: JSON.stringify(params)
data: JSON.stringify(data)
processData: true
success: (data, status, xhr) =>
if data.content_messages or not @set_active
@ -340,6 +351,8 @@ class ChannelInboundEdit extends App.ControllerModal
if @set_active
params['active'] = true
@processDestinationGroupEmailAddressParams(params)
# update
@ajax(
id: 'channel_email_verify'
@ -357,6 +370,8 @@ class ChannelInboundEdit extends App.ControllerModal
)
class ChannelGroupEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
buttonClose: true
buttonCancel: true
buttonSubmit: true
@ -364,13 +379,15 @@ class ChannelGroupEdit extends App.ControllerModal
content: =>
configureAttributesBase = [
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
]
@form = new App.ControllerForm(
model:
configure_attributes: configureAttributesBase
className: ''
params: @item
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
@ -388,6 +405,8 @@ class ChannelGroupEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
@processDestinationGroupEmailAddressParams(params)
# disable form
@formDisable(e)
@ -413,6 +432,8 @@ class AppConfig extends App.ControllerModal
button: 'Connect'
buttonCancel: true
small: true
events:
'click .js-copy': 'copyToClipboard'
content: ->
@external_credential = App.ExternalCredential.findByAttribute('name', 'google')
@ -425,6 +446,15 @@ class AppConfig extends App.ControllerModal
)
content
copyToClipboard: (e) =>
e.preventDefault()
button = $(e.target).parents('[role="button"]')
field_name = button.data('targetField')
value = $(@container).find("input[name='#{jQuery.escapeSelector(field_name)}']").val()
@copyToClipboardWithTooltip(value, e.target,'.modal-body', true)
onClosed: =>
return if !@isChanged
@isChanged = false

View file

@ -291,6 +291,8 @@ class ChannelAccountOverview extends App.ControllerSubContent
)
class ChannelInboundEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
buttonClose: true
buttonCancel: true
buttonSubmit: true
@ -298,14 +300,20 @@ class ChannelInboundEdit extends App.ControllerModal
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 },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@item.id, @item.group_id) },
{ 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
params: _.extend(
@item.options.inbound
group_id: @item.group_id
)
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
@ -324,6 +332,9 @@ class ChannelInboundEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
data =
options: params.options
# disable form
@formDisable(e)
@ -332,7 +343,7 @@ class ChannelInboundEdit extends App.ControllerModal
id: 'channel_email_inbound'
type: 'POST'
url: "#{@apiPath}/channels_microsoft365_inbound/#{@item.id}"
data: JSON.stringify(params)
data: JSON.stringify(data)
processData: true
success: (data, status, xhr) =>
if data.content_messages or not @set_active
@ -362,6 +373,8 @@ class ChannelInboundEdit extends App.ControllerModal
if @set_active
params['active'] = true
@processDestinationGroupEmailAddressParams(params)
# update
@ajax(
id: 'channel_email_verify'
@ -384,6 +397,8 @@ class ChannelInboundEdit extends App.ControllerModal
@navigate '#channels/microsoft365'
class ChannelGroupEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
buttonClose: true
buttonCancel: true
buttonSubmit: true
@ -391,13 +406,15 @@ class ChannelGroupEdit extends App.ControllerModal
content: =>
configureAttributesBase = [
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
]
@form = new App.ControllerForm(
model:
configure_attributes: configureAttributesBase
className: ''
params: @item
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
@ -415,6 +432,8 @@ class ChannelGroupEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
@processDestinationGroupEmailAddressParams(params)
# disable form
@formDisable(e)
@ -440,6 +459,8 @@ class AppConfig extends App.ControllerModal
button: 'Connect'
buttonCancel: true
small: true
events:
'click .js-copy': 'copyToClipboard'
content: ->
@external_credential = App.ExternalCredential.findByAttribute('name', 'microsoft365')
@ -452,6 +473,15 @@ class AppConfig extends App.ControllerModal
)
content
copyToClipboard: (e) =>
e.preventDefault()
button = $(e.target).parents('[role="button"]')
field_name = button.data('targetField')
value = $(@container).find("input[name='#{jQuery.escapeSelector(field_name)}']").val()
@copyToClipboardWithTooltip(value, e.target,'.modal-body', true)
onClosed: =>
return if !@isChanged
@isChanged = false

View file

@ -258,6 +258,8 @@ class ChannelAccountOverview extends App.ControllerSubContent
)
class ChannelGroupEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
buttonClose: true
buttonCancel: true
buttonSubmit: true
@ -266,12 +268,14 @@ class ChannelGroupEdit extends App.ControllerModal
content: =>
configureAttributesBase = [
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', options: @emailAddressOptions(@item.id, @item.group_id) },
]
@form = new App.ControllerForm(
model:
configure_attributes: configureAttributesBase
className: ''
params: @item
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
@ -289,6 +293,8 @@ class ChannelGroupEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
@processDestinationGroupEmailAddressParams(params)
# disable form
@formDisable(e)
@ -411,6 +417,8 @@ class ChannelInboundNew extends App.ControllerModal
window.location.href = "#{@apiPath}/external_credentials/microsoft_graph/link_account#{query_string}"
class ChannelInboundEdit extends App.ControllerModal
@include App.DestinationGroupEmailAddressesMixin
buttonClose: true
buttonCancel: true
buttonSubmit: __('Save')
@ -469,9 +477,10 @@ class ChannelInboundEdit extends App.ControllerModal
return App.view('microsoft_graph/error_message')(error: @error)
configureAttributesBase = [
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'options::folder_id', display: __('Folder'), tag: 'tree_select', null: true, options: @folderOptions, nulloption: true, default: '', help: __('Specify which folder to fetch from, or leave empty to fetch from ||inbox||.') },
{ name: 'options::keep_on_server', display: __('Keep messages on server'), tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false },
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', filter: { active: true } },
{ name: 'group_email_address_id', display: __('Destination Group Email Address'), tag: 'select', null: false, options: @emailAddressOptions(@item.id, @item.group_id) },
{ name: 'options::folder_id', display: __('Folder'), tag: 'tree_select', null: true, options: @folderOptions, nulloption: true, default: '', help: __('Specify which folder to fetch from, or leave empty to fetch from ||inbox||.') },
{ 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:
@ -482,6 +491,7 @@ class ChannelInboundEdit extends App.ControllerModal
options:
folder_id: @item.options.inbound.options.folder_id,
keep_on_server: @item.options.inbound.options.keep_on_server,
handlers: [@destinationGroupEmailAddressFormHandler(@item)]
)
@form.form
@ -498,6 +508,9 @@ class ChannelInboundEdit extends App.ControllerModal
@formValidate(form: e.target, errors: errors)
return false
data =
options: params.options
# disable form
@formDisable(e)
@ -508,7 +521,7 @@ class ChannelInboundEdit extends App.ControllerModal
id: 'channel_email_inbound'
type: 'POST'
url: "#{@apiPath}/channels/admin/microsoft_graph/inbound/#{@item.id}"
data: JSON.stringify(params)
data: JSON.stringify(data)
processData: true
success: (data, status, xhr) =>
if data.content_messages or not @set_active
@ -538,6 +551,8 @@ class ChannelInboundEdit extends App.ControllerModal
if @set_active
params['active'] = true
@processDestinationGroupEmailAddressParams(params)
# update
@ajax(
id: 'channel_email_verify'

View file

@ -0,0 +1,44 @@
# Common event handlers for destination group email address field.
App.DestinationGroupEmailAddressesMixin =
destinationGroupEmailAddressFormHandler: (item) ->
formHandler = (params, attribute, attributes, classname, form, ui) =>
return if not item?.id or attribute.name isnt 'group_id'
return if ui.FormHandlerGroupEmailAddressDone
ui.FormHandlerGroupEmailAddressDone = true
$(form).find('[name=group_id]').off('change.group_id').on('change.group_id', (e) =>
group_id = $(e.target).val()
for attr in attributes
continue if attr.name isnt 'group_email_address_id'
attr.options = @emailAddressOptions(item.id, group_id)
newElement = ui.formGenItem(attr, classname, form)
form.find('div.form-group[data-attribute-name="' + attr.name + '"]').replaceWith(newElement)
)
formHandler
emailAddressOptions: (id, group_id) ->
if !id
return {
false: App.i18n.translatePlain('Do not change email address')
true: App.i18n.translatePlain('Change to channel email address')
}
group = App.Group.find(group_id)
emailAddresses = App.EmailAddress.findAllByAttribute('channel_id', id)
_.reduce(
emailAddresses,
(acc, emailAddress) ->
return acc if emailAddress.id is group?.email_address_id
acc[emailAddress.id] = App.i18n.translatePlain('Change to %s', emailAddress.email)
acc
{ false: App.i18n.translatePlain('Do not change email address') }
)
processDestinationGroupEmailAddressParams: (params) ->
return if _.isUndefined(params.group_email_address_id)
params.group_email_address = params.group_email_address_id isnt 'false'
return if params.group_email_address and params.group_email_address_id isnt 'true'
delete params.group_email_address_id

View file

@ -15,7 +15,7 @@
</div>
<div class="modal-footer">
<div class="modal-leftFooter">
<button class="btn btn--text btn--secondary align-left js-expert"><%- @T('Experts') %></button>
<button class="btn btn--text btn--secondary align-left js-expert" type="button"><%- @T('Experts') %></button>
</div>
<div class="modal-rightFooter">
<button class="btn btn--primary align-right js-submit"><%- @T('Connect') %></button>

View file

@ -17,13 +17,18 @@
<label for="client_secret"><%- @T('Google Client Secret') %> <span>*</span></label>
</div>
<div class="controls">
<input id="client_secret" type="text" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
<input id="client_secret" type="password" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
</div>
</div>
<h2><%- @T('Your callback URL') %></h2>
<div class="input form-group">
<div class="controls">
<input class="form-control js-select" readonly value="<%= @callbackUrl %>" name="callback_url">
<div class="controls controls--button ignore-readonly">
<input readonly id="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
<span class="controls-button-inner">
<%- @Icon('clipboard') %>
</span>
</div>
</div>
</div>
</fieldset>

View file

@ -17,7 +17,7 @@
<label for="client_secret"><%- @T('Client Secret') %> <span>*</span></label>
</div>
<div class="controls">
<input id="client_secret" type="text" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
<input id="client_secret" type="password" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
</div>
</div>
<div class="input form-group">
@ -30,8 +30,13 @@
</div>
<h2><%- @T('Your callback URL') %></h2>
<div class="input form-group">
<div class="controls">
<input class="form-control js-select" readonly value="<%= @callbackUrl %>" name="callback_url">
<div class="controls controls--button ignore-readonly">
<input readonly id="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
<span class="controls-button-inner">
<%- @Icon('clipboard') %>
</span>
</div>
</div>
</div>
</fieldset>

View file

@ -31,7 +31,7 @@
<h2><%- @T('Your callback URL') %></h2>
<div class="input form-group">
<div class="controls controls--button ignore-readonly">
<input readonly id="callback_url" name="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
<input readonly id="callback_url" name="callback_url" type="text" value="<%= @callbackUrl %>" class="form-control">
<div class="controls-button u-clickable js-copy" role="button" data-target-field="callback_url" aria-label="<%- @T('Copy to clipboard') %>">
<span class="controls-button-inner">
<%- @Icon('clipboard') %>

View file

@ -1,74 +1,14 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
class ChannelsAdmin::MicrosoftGraphController < ChannelsAdmin::BaseController
include CanXoauth2EmailChannel
def area
'MicrosoftGraph::Account'.freeze
end
def index
system_online_service = Setting.get('system_online_service')
assets = {}
external_credential_ids = []
ExternalCredential.where(name: 'microsoft_graph').each do |external_credential|
assets = external_credential.assets(assets)
external_credential_ids.push external_credential.id
end
channels = Service::Channel::Admin::List.new(area: area).execute
channel_ids = []
channels.each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
not_used_email_address_ids = []
EmailAddress.find_each do |email_address|
next if system_online_service && email_address.preferences && email_address.preferences['online_service_disable']
assets = email_address.assets(assets)
if !email_address.channel_id || !email_address.active || !Channel.exists?(email_address.channel_id)
not_used_email_address_ids.push email_address.id
end
end
render json: {
assets: assets,
not_used_email_address_ids: not_used_email_address_ids,
channel_ids: channel_ids,
external_credential_ids: external_credential_ids,
callback_url: ExternalCredential.callback_url('microsoft_graph'),
}
end
def inbound
channel = Channel.find_by(id: params[:id], area:)
channel.refresh_xoauth2!
inbound_prepare_channel(channel, params)
result = EmailHelper::Probe.inbound(channel.options[:inbound])
raise Exceptions::UnprocessableEntity, (result[:message_human] || result[:message]) if result[:result] == 'invalid'
render json: result
end
def verify
channel = Channel.find_by(id: params[:id], area:)
verify_prepare_channel(channel, params)
channel.save!
render json: {}
end
def group
channel = Channel.find_by(id: params[:id], area:)
channel.group_id = params[:group_id]
channel.save!
render json: {}
def external_credential_name
'microsoft_graph'.freeze
end
def folders
@ -93,35 +33,4 @@ class ChannelsAdmin::MicrosoftGraphController < ChannelsAdmin::BaseController
render json: { folders:, error: }
end
private
def inbound_prepare_channel(channel, params)
channel.group_id = params[:group_id] if params[:group_id].present?
channel.active = params[:active] if params.key?(:active)
channel.options[:inbound] ||= {}
channel.options[:inbound][:options] ||= {}
%w[folder_id keep_on_server].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
end
def verify_prepare_channel(channel, params)
inbound_prepare_channel(channel, params)
%w[archive archive_before archive_state_id].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
channel.status_in = 'ok'
channel.status_out = 'ok'
channel.last_log_in = nil
channel.last_log_out = nil
end
end

View file

@ -81,7 +81,7 @@ class ChannelsEmailController < ApplicationController
return if params[:channel_id] && !check_access(params[:channel_id])
# connection test
result = EmailHelper::Probe.inbound(params)
result = EmailHelper::Probe.inbound(params.permit!.to_h)
# check account duplicate
return if account_duplicate?({ setting: { inbound: params } }, params[:channel_id])
@ -133,6 +133,9 @@ class ChannelsEmailController < ApplicationController
status_in: 'ok',
status_out: 'ok',
)
handle_group_email_address(channel, params)
render json: result
return
end
@ -143,6 +146,7 @@ class ChannelsEmailController < ApplicationController
group: ::Group.find(params[:group_id]),
email_address: email,
email_realname: params[:meta][:realname],
group_email_address: handle_group_email_address?,
)
render json: result
@ -173,6 +177,9 @@ class ChannelsEmailController < ApplicationController
channel = Channel.find_by(id: params[:id], area: 'Email::Account')
channel.group_id = params[:group_id]
channel.save!
handle_group_email_address(channel, params)
render json: {}
end
@ -239,4 +246,22 @@ class ChannelsEmailController < ApplicationController
raise Exceptions::Forbidden
end
def handle_group_email_address(channel)
return if !handle_group_email_address?
if params[:group_email_address_id]
email_address = EmailAddress.find(params[:group_email_address_id])
end
Service::Channel::Email::UpdateDestinationGroupEmail.new(
group: Group.find(params[:group_id]),
channel: channel,
email_address:,
).execute
end
def handle_group_email_address?
ActiveModel::Type::Boolean.new.cast params[:group_email_address]
end
end

View file

@ -1,128 +1,45 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
class ChannelsGoogleController < ApplicationController
include CanXoauth2EmailChannel
prepend_before_action :authenticate_and_authorize!
def index
system_online_service = Setting.get('system_online_service')
def area
'Google::Account'.freeze
end
assets = {}
external_credential_ids = []
ExternalCredential.where(name: 'google').each do |external_credential|
assets = external_credential.assets(assets)
external_credential_ids.push external_credential.id
end
channel_ids = []
Channel.where(area: 'Google::Account').reorder(:id).each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
not_used_email_address_ids = []
EmailAddress.find_each do |email_address|
next if system_online_service && email_address.preferences && email_address.preferences['online_service_disable']
assets = email_address.assets(assets)
if !email_address.channel_id || !email_address.active || !Channel.exists?(email_address.channel_id)
not_used_email_address_ids.push email_address.id
end
end
render json: {
assets: assets,
not_used_email_address_ids: not_used_email_address_ids,
channel_ids: channel_ids,
external_credential_ids: external_credential_ids,
callback_url: ExternalCredential.callback_url('google'),
}
def external_credential_name
'google'.freeze
end
def enable
channel = Channel.find_by(id: params[:id], area: 'Google::Account')
channel = Channel.find_by(id: params[:id], area:)
channel.active = true
channel.save!
render json: {}
end
def disable
channel = Channel.find_by(id: params[:id], area: 'Google::Account')
channel = Channel.find_by(id: params[:id], area:)
channel.active = false
channel.save!
render json: {}
end
def destroy
channel = Channel.find_by(id: params[:id], area: 'Google::Account')
channel = Channel.find_by(id: params[:id], area:)
email = EmailAddress.find_by(channel_id: channel.id)
email&.destroy!
channel.destroy!
render json: {}
end
def group
channel = Channel.find_by(id: params[:id], area: 'Google::Account')
channel.group_id = params[:group_id]
channel.save!
render json: {}
end
def inbound
channel = Channel.find_by(id: params[:id], area: 'Google::Account')
channel.refresh_xoauth2!(force: true)
channel.options[:inbound] ||= {}
channel.options[:inbound][:options] ||= {}
%w[folder keep_on_server].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
result = EmailHelper::Probe.inbound(channel.options[:inbound])
raise Exceptions::UnprocessableEntity, (result[:message_human] || result[:message]) if result[:result] == 'invalid'
render json: result
end
def verify
channel = Channel.find_by(id: params[:id], area: 'Google::Account')
verify_prepare_channel(channel, params)
channel.save!
render json: {}
end
def rollback_migration
channel = Channel.find_by!(id: params[:id], area: 'Google::Account')
channel = Channel.find_by!(id: params[:id], area:)
raise __('Failed to find backup on channel!') if !channel.options[:backup_imap_classic]
channel.update!(channel.options[:backup_imap_classic][:attributes])
render json: {}
end
private
def verify_prepare_channel(channel, params)
channel.group_id = params[:group_id] if params[:group_id].present?
channel.active = params[:active] if params.key?(:active)
channel.options[:inbound] ||= {}
channel.options[:inbound][:options] ||= {}
%w[folder keep_on_server archive archive_before archive_state_id].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
channel.status_in = 'ok'
channel.status_out = 'ok'
channel.last_log_in = nil
channel.last_log_out = nil
end
end

View file

@ -1,41 +1,16 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
class ChannelsMicrosoft365Controller < ApplicationController
include CanXoauth2EmailChannel
prepend_before_action :authenticate_and_authorize!
def index
system_online_service = Setting.get('system_online_service')
def area
'Microsoft365::Account'.freeze
end
assets = {}
external_credential_ids = []
ExternalCredential.where(name: 'microsoft365').each do |external_credential|
assets = external_credential.assets(assets)
external_credential_ids.push external_credential.id
end
channel_ids = []
Channel.where(area: 'Microsoft365::Account').reorder(:id).each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
not_used_email_address_ids = []
EmailAddress.find_each do |email_address|
next if system_online_service && email_address.preferences && email_address.preferences['online_service_disable']
assets = email_address.assets(assets)
if !email_address.channel_id || !email_address.active || !Channel.exists?(email_address.channel_id)
not_used_email_address_ids.push email_address.id
end
end
render json: {
assets: assets,
not_used_email_address_ids: not_used_email_address_ids,
channel_ids: channel_ids,
external_credential_ids: external_credential_ids,
callback_url: ExternalCredential.callback_url('microsoft365'),
}
def external_credential_name
'microsoft365'.freeze
end
def enable
@ -60,69 +35,11 @@ class ChannelsMicrosoft365Controller < ApplicationController
render json: {}
end
def group
channel = Channel.find_by(id: params[:id], area: 'Microsoft365::Account')
channel.group_id = params[:group_id]
channel.save!
render json: {}
end
def inbound
channel = Channel.find_by(id: params[:id], area: 'Microsoft365::Account')
channel.refresh_xoauth2!(force: true)
channel.options[:inbound] ||= {}
channel.options[:inbound][:options] ||= {}
%w[folder keep_on_server].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
result = EmailHelper::Probe.inbound(channel.options[:inbound])
raise Exceptions::UnprocessableEntity, (result[:message_human] || result[:message]) if result[:result] == 'invalid'
render json: result
end
def verify
channel = Channel.find_by(id: params[:id], area: 'Microsoft365::Account')
verify_prepare_channel(channel, params)
channel.save!
render json: {}
end
def rollback_migration
channel = Channel.find_by(id: params[:id], area: 'Microsoft365::Account')
channel = Channel.find_by(id: params[:id], area:)
raise __('Failed to find backup on channel!') if !channel.options[:backup_imap_classic]
channel.update!(channel.options[:backup_imap_classic][:attributes])
render json: {}
end
private
def verify_prepare_channel(channel, params)
channel.group_id = params[:group_id] if params[:group_id].present?
channel.active = params[:active] if params.key?(:active)
channel.options[:inbound] ||= {}
channel.options[:inbound][:options] ||= {}
%w[folder keep_on_server archive archive_before archive_state_id].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
channel.status_in = 'ok'
channel.status_out = 'ok'
channel.last_log_in = nil
channel.last_log_out = nil
end
end

View file

@ -0,0 +1,132 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
module CanXoauth2EmailChannel
extend ActiveSupport::Concern
def area
raise NotImplementedError
end
def external_credential_name
raise NotImplementedError
end
def index
system_online_service = Setting.get('system_online_service')
assets = {}
external_credential_ids = []
ExternalCredential.where(name: external_credential_name).each do |external_credential|
assets = external_credential.assets(assets)
external_credential_ids.push external_credential.id
end
channel_ids = []
Channel.where(area:).reorder(:id).each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
not_used_email_address_ids = []
EmailAddress.find_each do |email_address|
next if system_online_service && email_address.preferences && email_address.preferences['online_service_disable']
assets = email_address.assets(assets)
if !email_address.channel_id || !email_address.active || !Channel.exists?(email_address.channel_id)
not_used_email_address_ids.push email_address.id
end
end
render json: {
assets: assets,
not_used_email_address_ids: not_used_email_address_ids,
channel_ids: channel_ids,
external_credential_ids: external_credential_ids,
callback_url: ExternalCredential.callback_url(external_credential_name),
}
end
def group
channel = Channel.find_by(id: params[:id], area:)
channel.group_id = params[:group_id]
channel.save!
handle_group_email_address(channel)
render json: {}
end
def inbound
channel = Channel.find_by(id: params[:id], area:)
channel.refresh_xoauth2!(force: true)
inbound_prepare_channel(channel)
result = EmailHelper::Probe.inbound(channel.options[:inbound])
raise Exceptions::UnprocessableEntity, (result[:message_human] || result[:message]) if result[:result] == 'invalid'
render json: result
end
def verify
channel = Channel.find_by(id: params[:id], area:)
verify_prepare_channel(channel)
channel.save!
handle_group_email_address(channel)
render json: {}
end
private
def inbound_prepare_channel(channel)
channel.group_id = params[:group_id] if params[:group_id].present?
channel.active = params[:active] if params.key?(:active)
channel.options[:inbound] ||= {}
channel.options[:inbound][:options] ||= {}
%w[folder folder_id keep_on_server].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
end
def verify_prepare_channel(channel)
inbound_prepare_channel(channel)
%w[archive archive_before archive_state_id].each do |key|
next if params.dig(:options, key).nil?
channel.options[:inbound][:options][key] = params[:options][key]
end
channel.status_in = 'ok'
channel.status_out = 'ok'
channel.last_log_in = nil
channel.last_log_out = nil
end
def handle_group_email_address(channel)
return if !handle_group_email_address?
if params[:group_email_address_id]
email_address = EmailAddress.find(params[:group_email_address_id])
end
Service::Channel::Email::UpdateDestinationGroupEmail.new(
group: Group.find(params[:group_id]),
channel: channel,
email_address:,
).execute
end
def handle_group_email_address?
ActiveModel::Type::Boolean.new.cast params[:group_email_address]
end
end

View file

@ -2,9 +2,9 @@
class Service::Channel::Email::Create < Service::Base
def execute(inbound_configuration:, outbound_configuration:, group:, email_address:, email_realname:)
def execute(inbound_configuration:, outbound_configuration:, group:, email_address:, email_realname:, group_email_address: false)
::Channel.create!(
new_channel = ::Channel.create!(
area: 'Email::Account',
options: {
inbound: inbound_configuration,
@ -19,6 +19,12 @@ class Service::Channel::Email::Create < Service::Base
).tap do |channel|
set_email_address(channel:, email_address:, email_realname:)
end
if group_email_address
Service::Channel::Email::UpdateDestinationGroupEmail.new(group:, channel: new_channel).execute
end
new_channel
end
private

View file

@ -0,0 +1,19 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
class Service::Channel::Email::UpdateDestinationGroupEmail < Service::Base
attr_reader :group, :channel, :email_address
def initialize(group:, channel:, email_address: nil)
super()
@channel = channel
@group = group
@email_address = email_address || EmailAddress.find_by(channel_id: channel.id)
end
def execute
return if email_address.nil?
group.update!(email_address: email_address)
end
end

View file

@ -224,7 +224,7 @@ msgstr ""
msgid "%s days ago"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:661
#: app/assets/javascripts/app/controllers/_channel/email.coffee:715
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:260
#: app/assets/javascripts/app/views/channel/email_archive.jst.eco:4
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundMessagesForm.ts:28
@ -729,9 +729,9 @@ msgstr ""
msgid "Account Time"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:581
#: app/assets/javascripts/app/controllers/_channel/email.coffee:624
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:197
#: app/controllers/channels_email_controller.rb:204
#: app/controllers/channels_email_controller.rb:214
msgid "Account already exists!"
msgstr ""
@ -1574,21 +1574,21 @@ msgstr ""
msgid "Archive Emails"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:690
#: app/assets/javascripts/app/controllers/_channel/email.coffee:744
#: app/assets/javascripts/app/controllers/_channel/email_archive.coffee:43
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:289
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundMessagesForm.ts:58
msgid "Archive cut-off time"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:689
#: app/assets/javascripts/app/controllers/_channel/email.coffee:743
#: app/assets/javascripts/app/controllers/_channel/email_archive.coffee:42
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:288
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundMessagesForm.ts:44
msgid "Archive emails"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:691
#: app/assets/javascripts/app/controllers/_channel/email.coffee:745
#: app/assets/javascripts/app/controllers/_channel/email_archive.coffee:44
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:290
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundMessagesForm.ts:74
@ -2613,6 +2613,14 @@ msgstr ""
msgid "Change text color"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:268
msgid "Change to %s"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:258
msgid "Change to channel email address"
msgstr ""
#: app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/actions/TicketHistory/HistoryEventDetails/HistoryEventDetailsReaction.vue:17
msgid "Changed reaction on message %s from %s"
msgstr ""
@ -2645,7 +2653,7 @@ msgstr ""
msgid "Changes were made that require a database update."
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:258
#: app/assets/javascripts/app/controllers/_channel/email.coffee:281
#: app/assets/javascripts/app/controllers/_channel/google.coffee:275
#: app/assets/javascripts/app/controllers/_channel/microsoft365.coffee:297
#: app/assets/javascripts/app/controllers/_channel/microsoft_graph.coffee:264
@ -3430,7 +3438,7 @@ msgstr ""
msgid "Could not fetch security keys"
msgstr ""
#: app/controllers/channels_admin/microsoft_graph_controller.rb:76
#: app/controllers/channels_admin/microsoft_graph_controller.rb:78
msgid "Could not find the channel."
msgstr ""
@ -3439,7 +3447,7 @@ msgstr ""
msgid "Could not generate recovery codes"
msgstr ""
#: app/controllers/channels_admin/microsoft_graph_controller.rb:79
#: app/controllers/channels_admin/microsoft_graph_controller.rb:81
msgid "Could not identify the channel mailbox."
msgstr ""
@ -4981,7 +4989,7 @@ msgstr ""
msgid "Desktop"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:262
#: app/assets/javascripts/app/controllers/_channel/email.coffee:285
#: app/assets/javascripts/app/controllers/_channel/google.coffee:367
#: app/assets/javascripts/app/controllers/_channel/microsoft365.coffee:394
#: app/assets/javascripts/app/controllers/_channel/microsoft_graph.coffee:268
@ -4994,6 +5002,10 @@ msgstr ""
msgid "Destination Group"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:286
msgid "Destination Group Email Address"
msgstr ""
#: app/assets/javascripts/app/views/integration/cti.jst.eco:58
#: app/assets/javascripts/app/views/integration/placetel.jst.eco:74
#: app/assets/javascripts/app/views/integration/sipgate.jst.eco:61
@ -5178,6 +5190,10 @@ msgstr ""
msgid "Display name"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:257
msgid "Do not change email address"
msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee:662
msgid "Do not encrypt email"
msgstr ""
@ -5637,7 +5653,7 @@ msgstr ""
msgid "Email could not be verified. Please contact your administrator."
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:855
#: app/assets/javascripts/app/controllers/_channel/email.coffee:912
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:404
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailChannelConfiguration.ts:169
msgid "Email sending and receiving could not be verified. Please check your settings."
@ -5656,7 +5672,7 @@ msgstr ""
msgid "Email sent to %s"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:690
#: app/assets/javascripts/app/controllers/_channel/email.coffee:744
#: app/assets/javascripts/app/controllers/_channel/email_archive.coffee:43
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:289
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundMessagesForm.ts:61
@ -6333,8 +6349,8 @@ msgstr ""
msgid "Failed to delete checklist item."
msgstr ""
#: app/controllers/channels_google_controller.rb:102
#: app/controllers/channels_microsoft365_controller.rb:102
#: app/controllers/channels_google_controller.rb:97
#: app/controllers/channels_microsoft365_controller.rb:97
msgid "Failed to find backup on channel!"
msgstr ""
@ -6534,7 +6550,7 @@ msgstr ""
msgid "Flat Design"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:418
#: app/assets/javascripts/app/controllers/_channel/email.coffee:450
#: app/assets/javascripts/app/controllers/_channel/google.coffee:279
#: app/assets/javascripts/app/controllers/_channel/microsoft365.coffee:301
#: app/assets/javascripts/app/controllers/_channel/microsoft_graph.coffee:473
@ -7337,7 +7353,7 @@ msgstr ""
msgid "Home"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:412
#: app/assets/javascripts/app/controllers/_channel/email.coffee:444
#: app/assets/javascripts/app/controllers/_integration/ldap.coffee:325
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:80
#: app/assets/javascripts/app/controllers/getting_started/email_notification.coffee:69
@ -8202,7 +8218,7 @@ msgstr ""
msgid "Kayako"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:419
#: app/assets/javascripts/app/controllers/_channel/email.coffee:451
#: app/assets/javascripts/app/controllers/_channel/google.coffee:280
#: app/assets/javascripts/app/controllers/_channel/microsoft365.coffee:302
#: app/assets/javascripts/app/controllers/_channel/microsoft_graph.coffee:474
@ -10028,7 +10044,7 @@ msgstr ""
msgid "No Proxy"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:415
#: app/assets/javascripts/app/controllers/_channel/email.coffee:447
#: app/assets/javascripts/app/controllers/_integration/ldap.coffee:203
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:83
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundForm.ts:43
@ -10853,7 +10869,7 @@ msgstr ""
msgid "Organization"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:382
#: app/assets/javascripts/app/controllers/_channel/email.coffee:410
msgid "Organization & Department Name"
msgstr ""
@ -10861,7 +10877,7 @@ msgstr ""
msgid "Organization Name"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:382
#: app/assets/javascripts/app/controllers/_channel/email.coffee:410
#: app/assets/javascripts/app/views/getting_started/email.jst.eco:12
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailAccountForm.ts:27
msgid "Organization Support"
@ -11126,7 +11142,7 @@ msgstr ""
msgid "Parent group"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:384
#: app/assets/javascripts/app/controllers/_channel/email.coffee:412
#: app/assets/javascripts/app/controllers/_manage/security.coffee:10
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:82
#: app/assets/javascripts/app/controllers/getting_started/email_notification.coffee:71
@ -11522,7 +11538,7 @@ msgstr ""
msgid "Please wait…"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:417
#: app/assets/javascripts/app/controllers/_channel/email.coffee:449
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:85
#: app/assets/javascripts/app/controllers/getting_started/email_notification.coffee:72
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundForm.ts:161
@ -12523,7 +12539,7 @@ msgstr ""
msgid "SMTP - configure your own outgoing SMTP settings"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:415
#: app/assets/javascripts/app/controllers/_channel/email.coffee:447
#: app/assets/javascripts/app/controllers/_integration/ldap.coffee:207
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:83
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundForm.ts:47
@ -12535,7 +12551,7 @@ msgstr ""
msgid "SSL Certificates"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:416
#: app/assets/javascripts/app/controllers/_channel/email.coffee:448
#: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee:714
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:84
#: app/assets/javascripts/app/controllers/getting_started/email_notification.coffee:73
@ -12551,7 +12567,7 @@ msgstr ""
msgid "SSL verification"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:415
#: app/assets/javascripts/app/controllers/_channel/email.coffee:447
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:83
#: app/assets/javascripts/app/views/integration/ldap.jst.eco:27
#: app/assets/javascripts/app/views/integration/ldap_wizard.jst.eco:33
@ -12565,7 +12581,7 @@ msgstr ""
msgid "SSO"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:415
#: app/assets/javascripts/app/controllers/_channel/email.coffee:447
#: app/assets/javascripts/app/controllers/_integration/ldap.coffee:205
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:83
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailInboundForm.ts:54
@ -13058,7 +13074,7 @@ msgstr ""
msgid "Send Invitation"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:397
#: app/assets/javascripts/app/controllers/_channel/email.coffee:427
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:65
#: app/assets/javascripts/app/controllers/getting_started/email_notification.coffee:50
msgid "Send Mails via"
@ -14640,7 +14656,7 @@ msgstr ""
msgid "The certificates for %s were not found."
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:302
#: app/assets/javascripts/app/controllers/_channel/email.coffee:328
#: app/assets/javascripts/app/controllers/_channel/facebook.coffee:246
#: app/assets/javascripts/app/controllers/_channel/google.coffee:334
#: app/assets/javascripts/app/controllers/_channel/microsoft365.coffee:356
@ -15299,7 +15315,7 @@ msgstr ""
msgid "The server presented a certificate that could not be verified."
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:584
#: app/assets/javascripts/app/controllers/_channel/email.coffee:627
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:200
#: app/frontend/apps/desktop/entities/channel-email/composables/useEmailChannelConfiguration.ts:245
msgid "The server settings could not be automatically detected. Please configure them manually."
@ -16967,7 +16983,7 @@ msgstr ""
msgid "Two-factor method has been configured successfully."
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:411
#: app/assets/javascripts/app/controllers/_channel/email.coffee:443
#: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee:248
#: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee:79
#: app/assets/javascripts/app/controllers/translation.coffee:289
@ -17410,7 +17426,7 @@ msgstr ""
msgid "Used when viewing a Ticket"
msgstr ""
#: app/assets/javascripts/app/controllers/_channel/email.coffee:413
#: app/assets/javascripts/app/controllers/_channel/email.coffee:445
#: app/assets/javascripts/app/controllers/_manage/ticket_duplicate_detection.coffee:37
#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee:165
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee:24

View file

@ -76,8 +76,8 @@ RSpec.describe 'Microsoft Graph channel admin API endpoints', aggregate_failures
end
describe 'POST /api/v1/channels_admin/microsoft_graph/inbound/ID' do
let(:channel) { create(:microsoft_graph_channel) }
let(:group) { create(:group) }
let(:channel) { create(:microsoft_graph_channel) }
let(:group) { create(:group) }
before do
allow_any_instance_of(Channel).to receive(:refresh_xoauth2!).and_return(true)
@ -92,8 +92,9 @@ RSpec.describe 'Microsoft Graph channel admin API endpoints', aggregate_failures
end
describe 'POST /api/v1/channels_admin/microsoft_graph/verify/ID' do
let(:channel) { create(:microsoft_graph_channel) }
let(:group) { create(:group) }
let(:channel) { create(:microsoft_graph_channel) }
let(:group) { create(:group, email_address_id: nil) }
let!(:email_address) { create(:email_address, channel: channel) }
it 'updates inbound options of the channel' do
post "/api/v1/channels/admin/microsoft_graph/verify/#{channel.id}", params: { group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true', archive: 'true', archive_before: '2025-01-01T00.00.000Z', archive_state_id: Ticket::State.find_by(name: 'closed').id } }
@ -116,5 +117,34 @@ RSpec.describe 'Microsoft Graph channel admin API endpoints', aggregate_failures
)
)
end
context 'when group email address is used' do
it 'updates the group email address' do
post "/api/v1/channels/admin/microsoft_graph/verify/#{channel.id}", params: { group_email_address: true, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.group.reload.email_address_id).to eq(email_address.id)
end
context 'when group email should not be changed' do
it 'does not update the group email address' do
post "/api/v1/channels/admin/microsoft_graph/verify/#{channel.id}", params: { group_email_address: false, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.reload.group.email_address_id).to be_nil
end
end
context 'when group email should be changed to specific email address' do
let!(:email_address2) { create(:email_address, channel: channel) }
it 'updates the group email address' do
post "/api/v1/channels/admin/microsoft_graph/verify/#{channel.id}", params: { group_email_address: true, group_email_address_id: email_address2.id, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.reload.group.email_address_id).to eq(email_address2.id)
end
end
end
end
end

View file

@ -0,0 +1,115 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe 'Email channel API endpoints', type: :request do
let(:admin) { create(:admin) }
describe 'POST /api/v1/channels_email/verify/ID', aggregate_failures: true, authenticated_as: :admin do
let(:group) { create(:group) }
let(:params) do
{
inbound: inbound_params,
outbound: outbound_params,
meta: meta_params,
group_id: group_id,
group_email_address: group_email_address,
group_email_address_id: group_email_address_id,
}
end
let(:inbound_params) do
{
adapter: 'imap',
options: {
host: 'nonexisting.host.local',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'xyz',
ssl_verify: true,
archive: true,
archive_before: '2025-01-01T00.00.000Z',
archive_state_id: Ticket::State.find_by(name: 'open').id
},
}
end
let(:outbound_params) do
{
adapter: 'smtp',
options: {
host: 'nonexisting.host.local',
port: 465,
start_tls: true,
user: 'some@example.com',
password: 'xyz',
ssl_verify: true,
ssl: true,
domain: 'example.com',
enable_starttls_auto: true,
},
}
end
let(:meta_params) do
{
realname: 'Testing',
email: 'some@example.com',
password: 'xyz',
}
end
let(:group_id) { nil }
let(:group_email_address) { false }
let(:group_email_address_id) { nil }
before do
Channel.where(area: 'Email::Account').each(&:destroy)
allow(EmailHelper::Verify).to receive(:email).and_return({ result: 'ok' })
end
it 'creates new channel' do
post '/api/v1/channels_email_verify', params: params
expect(response).to have_http_status(:ok)
expect(Channel.last).to have_attributes(
group_id: Group.first.id,
options: include(
inbound: include(
options: include(
archive: 'true',
archive_before: '2025-01-01T00.00.000Z',
archive_state_id: Ticket::State.find_by(name: 'open').id.to_s,
)
)
)
)
end
context 'when group email address handling is used' do
let(:group) { create(:group, email_address: nil) }
let(:group_id) { group.id }
context 'when group email address should be set' do
let(:group_email_address) { true }
it 'creates new channel' do
post '/api/v1/channels_email_verify', params: params
expect(response).to have_http_status(:ok)
expect(Channel.last.group.email_address_id).to eq(EmailAddress.find_by(channel_id: Channel.last.id).id)
end
end
context 'when group email address should not be set' do
let(:group_email_address) { false }
it 'creates new channel' do
post '/api/v1/channels_email_verify', params: params
expect(response).to have_http_status(:ok)
expect(Channel.last.group.email_address_id).to be_nil
end
end
end
end
end

View file

@ -48,7 +48,7 @@ RSpec.describe 'Google channel API endpoints', type: :request do
end
end
describe 'POST /api/v1/channels_google/inbound/ID' do
describe 'POST /api/v1/channels_google_inbound/ID' do
let(:channel) { create(:google_channel) }
let(:group) { create(:group) }
@ -60,17 +60,20 @@ RSpec.describe 'Google channel API endpoints', type: :request do
it 'does not update inbound options of the channel' do
expect do
post "/api/v1/channels_google/inbound/#{channel.id}", params: { group_id: group.id, options: { folder: 'SomeFolder', keep_on_server: 'true' } }
post "/api/v1/channels_google_inbound/#{channel.id}", params: { group_id: group.id, options: { folder: 'SomeFolder', keep_on_server: 'true' } }
end.not_to change(channel, :updated_at)
end
end
describe 'POST /api/v1/channels_google/verify/ID', aggregate_failures: true, authenticated_as: :admin do
let(:channel) { create(:google_channel) }
let(:group) { create(:group) }
describe 'POST /api/v1/channels_google_verify/ID', aggregate_failures: true, authenticated_as: :admin do
let(:channel) { create(:google_channel) }
let(:group) { create(:group, email_address_id: nil) }
let(:email_address) { create(:email_address, channel: channel) }
before do
Channel.where(area: 'Google::Account').each(&:destroy)
email_address
end
it 'updates inbound options of the channel' do
@ -94,5 +97,34 @@ RSpec.describe 'Google channel API endpoints', type: :request do
)
)
end
context 'when group email address is used' do
it 'updates the group email address' do
post "/api/v1/channels_google_verify/#{channel.id}", params: { group_email_address: true, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.group.reload.email_address_id).to eq(email_address.id)
end
context 'when group email should not be changed' do
it 'does not update the group email address' do
post "/api/v1/channels_google_verify/#{channel.id}", params: { group_email_address: false, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.reload.group.email_address_id).to be_nil
end
end
context 'when group email should be changed to specific email address' do
let(:email_address2) { create(:email_address, channel: channel) }
it 'updates the group email address' do
post "/api/v1/channels_google_verify/#{channel.id}", params: { group_email_address: true, group_email_address_id: email_address2.id, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.reload.group.email_address_id).to eq(email_address2.id)
end
end
end
end
end

View file

@ -67,10 +67,13 @@ RSpec.describe 'Microsoft365 channel API endpoints', type: :request do
describe 'POST /api/v1/channels_microsoft365/verify/ID', aggregate_failures: true, authenticated_as: :admin do
let(:channel) { create(:microsoft365_channel) }
let(:group) { create(:group) }
let(:group) { create(:group, email_address_id: nil) }
let(:email_address) { create(:email_address, channel: channel) }
before do
Channel.where(area: 'Microsoft365::Account').each(&:destroy)
email_address
end
it 'updates inbound options of the channel' do
@ -94,5 +97,34 @@ RSpec.describe 'Microsoft365 channel API endpoints', type: :request do
)
)
end
context 'when group email address is used' do
it 'updates the group email address' do
post "/api/v1/channels_microsoft365_verify/#{channel.id}", params: { group_email_address: true, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.group.reload.email_address_id).to eq(email_address.id)
end
context 'when group email should not be changed' do
it 'does not update the group email address' do
post "/api/v1/channels_microsoft365_verify/#{channel.id}", params: { group_email_address: false, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.reload.group.email_address_id).to be_nil
end
end
context 'when group email should be changed to specific email address' do
let(:email_address2) { create(:email_address, channel: channel) }
it 'updates the group email address' do
post "/api/v1/channels_microsoft365_verify/#{channel.id}", params: { group_email_address: true, group_email_address_id: email_address2.id, group_id: group.id, options: { folder_id: 'AAMkAD=', keep_on_server: 'true' } }
expect(response).to have_http_status(:ok)
expect(channel.reload.group.email_address_id).to eq(email_address2.id)
end
end
end
end
end

View file

@ -0,0 +1,26 @@
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe Service::Channel::Email::UpdateDestinationGroupEmail, current_user_id: 1 do
subject(:service) { described_class.new(group:, channel:, email_address:) }
let(:channel) { create(:channel) }
let(:group) { create(:group) }
let(:email_address) { create(:email_address) }
describe '#execute' do
it 'update channel email address' do
expect { service.execute }.to change { group.reload.email_address_id }.to be(email_address.id)
end
context 'when email address is not given' do
let(:email_address) { nil }
let(:email_address2) { create(:email_address, channel: channel) }
it 'does update group email address from channel' do
expect { service.execute }.to change { group.reload.email_address_id }.to be(email_address2.id)
end
end
end
end

View file

@ -221,15 +221,13 @@ RSpec.describe 'Manage > Channels > Email', integration: true, type: :system do
end
context 'when editing inbound email settings' do
it 'the expert form fields are not shown' do
it 'does not display intro form fields' do
visit '#channels/email'
click '.js-channelEnable'
click '.js-editInbound'
in_modal do
expect(page).to have_no_text 'ORGANIZATION & DEPARTMENT NAME'
expect(page).to have_no_text 'ORGANIZATION SUPPORT'
expect(page).to have_no_text 'EMAIL'
end
end