Fixes #3695 - Detect Jira follow-ups.
This commit is contained in:
parent
86e18abd00
commit
7e6e751d69
8 changed files with 233 additions and 82 deletions
94
app/models/channel/filter/base_external_check.rb
Normal file
94
app/models/channel/filter/base_external_check.rb
Normal file
|
@ -0,0 +1,94 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Channel::Filter::BaseExternalCheck
|
||||
|
||||
# required mail header to start the detection
|
||||
MAIL_HEADER = 'x-example-header'.freeze
|
||||
|
||||
# regex to detect the id in the subject of the email
|
||||
SOURCE_ID_REGEX = %r{\s(EXAMPLE-MATCH\d+)\s}.freeze
|
||||
|
||||
# External sync references source name prefix
|
||||
SOURCE_NAME_PREFIX = 'Example'.freeze
|
||||
|
||||
# This filter will run pre and post
|
||||
def self.run(_channel, mail, ticket_or_transaction_params = nil, _article = nil, _session_user = nil)
|
||||
return if mail[const_get(:MAIL_HEADER)].blank?
|
||||
|
||||
source_id = self.source_id(subject: mail[:subject])
|
||||
return if source_id.blank?
|
||||
|
||||
source_name = self.source_name(from: mail[:from])
|
||||
return if source_name.blank?
|
||||
|
||||
# check if we can followup by existing service now relation
|
||||
if ticket_or_transaction_params.blank? || ticket_or_transaction_params.is_a?(Hash)
|
||||
from_sync_entry(
|
||||
mail: mail,
|
||||
source_name: source_name,
|
||||
source_id: source_id,
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
ExternalSync.create_with(source_id: source_id).find_or_create_by(source: source_name, object: 'Ticket', o_id: ticket_or_transaction_params.id)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
This function returns the source id of the service now email if given.
|
||||
|
||||
source_id = Channel::Filter::ServiceNowCheck.source_id(
|
||||
from: 'test@service-now.com',
|
||||
subject: 'Incident INC12345 --- test',
|
||||
)
|
||||
|
||||
returns:
|
||||
|
||||
source_id = 'INC12345'
|
||||
|
||||
=end
|
||||
|
||||
def self.source_id(subject: '')
|
||||
|
||||
# check if we can find the service now relation
|
||||
source_id = nil
|
||||
if subject =~ const_get(:SOURCE_ID_REGEX)
|
||||
source_id = $1
|
||||
end
|
||||
|
||||
source_id
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
This function returns the sync id of the service now email if given.
|
||||
|
||||
source_name = Channel::Filter::ServiceNowCheck.source_name(
|
||||
from: 'test@service-now.com',
|
||||
)
|
||||
|
||||
returns:
|
||||
|
||||
source_name = 'ServiceNow-test@service-now.com'
|
||||
|
||||
=end
|
||||
|
||||
def self.source_name(from:)
|
||||
address = Mail::AddressList.new(from).addresses.first.address.downcase
|
||||
"#{const_get(:SOURCE_NAME_PREFIX)}-#{address}"
|
||||
rescue => e
|
||||
Rails.logger.info "Unable to parse email address in '#{from}': #{e.message}"
|
||||
end
|
||||
|
||||
def self.from_sync_entry(mail:, source_name:, source_id:)
|
||||
sync_entry = ExternalSync.find_by(
|
||||
source: source_name,
|
||||
source_id: source_id,
|
||||
object: 'Ticket',
|
||||
)
|
||||
return if sync_entry.blank?
|
||||
|
||||
mail[ :'x-zammad-ticket-id' ] = sync_entry.o_id
|
||||
end
|
||||
end
|
7
app/models/channel/filter/jira_check.rb
Normal file
7
app/models/channel/filter/jira_check.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Channel::Filter::JiraCheck < Channel::Filter::BaseExternalCheck
|
||||
MAIL_HEADER = 'x-jira-fingerprint'.freeze
|
||||
SOURCE_ID_REGEX = %r{\[JIRA\]\s\((\w+-\d+)\)}.freeze
|
||||
SOURCE_NAME_PREFIX = 'Jira'.freeze
|
||||
end
|
|
@ -1,85 +1,7 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
module Channel::Filter::ServiceNowCheck
|
||||
|
||||
# This filter will run pre and post
|
||||
def self.run(_channel, mail, ticket_or_transaction_params = nil, _article = nil, _session_user = nil)
|
||||
return if mail['x-servicenow-generated'].blank?
|
||||
|
||||
source_id = self.source_id(subject: mail[:subject])
|
||||
return if source_id.blank?
|
||||
|
||||
source_name = self.source_name(from: mail[:from])
|
||||
return if source_name.blank?
|
||||
|
||||
# check if we can followup by existing service now relation
|
||||
if ticket_or_transaction_params.blank? || ticket_or_transaction_params.is_a?(Hash)
|
||||
from_sync_entry(
|
||||
mail: mail,
|
||||
source_name: source_name,
|
||||
source_id: source_id,
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
ExternalSync.create_with(source_id: source_id).find_or_create_by(source: source_name, object: 'Ticket', o_id: ticket_or_transaction_params.id)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
This function returns the source id of the service now email if given.
|
||||
|
||||
source_id = Channel::Filter::ServiceNowCheck.source_id(
|
||||
from: 'test@service-now.com',
|
||||
subject: 'Incident INC12345 --- test',
|
||||
)
|
||||
|
||||
returns:
|
||||
|
||||
source_id = 'INC12345'
|
||||
|
||||
=end
|
||||
|
||||
def self.source_id(subject: '')
|
||||
|
||||
# check if we can find the service now relation
|
||||
source_id = nil
|
||||
if subject =~ %r{\s(INC\d+)\s}
|
||||
source_id = $1
|
||||
end
|
||||
|
||||
source_id
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
This function returns the sync id of the service now email if given.
|
||||
|
||||
source_name = Channel::Filter::ServiceNowCheck.source_name(
|
||||
from: 'test@service-now.com',
|
||||
)
|
||||
|
||||
returns:
|
||||
|
||||
source_name = 'ServiceNow-test@service-now.com'
|
||||
|
||||
=end
|
||||
|
||||
def self.source_name(from:)
|
||||
address = Mail::AddressList.new(from).addresses.first.address.downcase
|
||||
"ServiceNow-#{address}"
|
||||
rescue => e
|
||||
Rails.logger.info "Unable to parse email address in '#{from}': #{e.message}"
|
||||
end
|
||||
|
||||
def self.from_sync_entry(mail:, source_name:, source_id:)
|
||||
sync_entry = ExternalSync.find_by(
|
||||
source: source_name,
|
||||
source_id: source_id,
|
||||
object: 'Ticket',
|
||||
)
|
||||
return if sync_entry.blank?
|
||||
|
||||
mail[ :'x-zammad-ticket-id' ] = sync_entry.o_id
|
||||
end
|
||||
class Channel::Filter::ServiceNowCheck < Channel::Filter::BaseExternalCheck
|
||||
MAIL_HEADER = 'x-servicenow-generated'.freeze
|
||||
SOURCE_ID_REGEX = %r{\s(INC\d+)\s}.freeze
|
||||
SOURCE_NAME_PREFIX = 'ServiceNow'.freeze
|
||||
end
|
||||
|
|
30
db/migrate/20210812000001_jira_config.rb
Normal file
30
db/migrate/20210812000001_jira_config.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class JiraConfig < ActiveRecord::Migration[4.2]
|
||||
def up
|
||||
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Defines postmaster filter.',
|
||||
name: '5400_postmaster_filter_jira_check',
|
||||
area: 'Postmaster::PreFilter',
|
||||
description: 'Defines postmaster filter to identify jira mails for correct follow-ups.',
|
||||
options: {},
|
||||
state: 'Channel::Filter::JiraCheck',
|
||||
frontend: false
|
||||
)
|
||||
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Defines postmaster filter.',
|
||||
name: '5401_postmaster_filter_jira_check',
|
||||
area: 'Postmaster::PostFilter',
|
||||
description: 'Defines postmaster filter to identify jira mails for correct follow-ups.',
|
||||
options: {},
|
||||
state: 'Channel::Filter::JiraCheck',
|
||||
frontend: false
|
||||
)
|
||||
end
|
||||
|
||||
end
|
|
@ -3538,6 +3538,24 @@ Setting.create_if_not_exists(
|
|||
state: 'Channel::Filter::ServiceNowCheck',
|
||||
frontend: false
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Defines postmaster filter.',
|
||||
name: '5400_postmaster_filter_jira_check',
|
||||
area: 'Postmaster::PreFilter',
|
||||
description: 'Defines postmaster filter to identify jira mails for correct follow-ups.',
|
||||
options: {},
|
||||
state: 'Channel::Filter::JiraCheck',
|
||||
frontend: false
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Defines postmaster filter.',
|
||||
name: '5401_postmaster_filter_jira_check',
|
||||
area: 'Postmaster::PostFilter',
|
||||
description: 'Defines postmaster filter to identify jira mails for correct follow-ups.',
|
||||
options: {},
|
||||
state: 'Channel::Filter::JiraCheck',
|
||||
frontend: false
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Define postmaster filter.',
|
||||
name: '5500_postmaster_internal_article_check',
|
||||
|
|
|
@ -1169,6 +1169,50 @@ RSpec.describe Channel::EmailParser, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Jira handling' do
|
||||
|
||||
context 'new Ticket' do
|
||||
let(:mail_file) { Rails.root.join('test/data/mail/mail103.box') }
|
||||
|
||||
it 'creates an ExternalSync reference' do
|
||||
described_class.new.process({}, raw_mail)
|
||||
|
||||
expect(ExternalSync.last).to have_attributes(
|
||||
source: 'Jira-example@jira.com',
|
||||
source_id: 'SYS-422',
|
||||
object: 'Ticket',
|
||||
o_id: Ticket.last.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'follow up' do
|
||||
|
||||
let(:mail_file) { Rails.root.join('test/data/mail/mail104.box') }
|
||||
let(:ticket) { create(:ticket) }
|
||||
let!(:external_sync) do
|
||||
create(:external_sync,
|
||||
source: 'Jira-example@jira.com',
|
||||
source_id: 'SYS-422',
|
||||
object: 'Ticket',
|
||||
o_id: ticket.id,)
|
||||
end
|
||||
|
||||
it 'adds Article to existing Ticket' do
|
||||
expect { described_class.new.process({}, raw_mail) }.to change { ticket.reload.articles.count }
|
||||
end
|
||||
|
||||
context 'key insensitive sender address' do
|
||||
|
||||
let(:raw_mail) { super().gsub('example@service-now.com', 'Example@Service-Now.com') }
|
||||
|
||||
it 'adds Article to existing Ticket' do
|
||||
expect { described_class.new.process({}, raw_mail) }.to change { ticket.reload.articles.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'XSS protection' do
|
||||
let(:article) { described_class.new.process({}, raw_mail).second }
|
||||
|
||||
|
|
18
test/data/mail/mail103.box
Normal file
18
test/data/mail/mail103.box
Normal file
|
@ -0,0 +1,18 @@
|
|||
From: IT example <example@jira.com>
|
||||
To: support@example.com
|
||||
Message-ID: <18659453.58107.1576665116411@app129169.gva3.jira.com>
|
||||
Subject: [JIRA] (SYS-422) test zammad 3
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_Part_58105_29005161.1576665006397"
|
||||
X-JIRA-FingerPrint: 978a1eb52201518931e77cb5725717ff
|
||||
|
||||
------=_Part_58105_29005161.1576665006397
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_58106_4969544.1576665006398"
|
||||
|
||||
------=_Part_58106_4969544.1576665006398
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Incident content
|
18
test/data/mail/mail104.box
Normal file
18
test/data/mail/mail104.box
Normal file
|
@ -0,0 +1,18 @@
|
|||
From: IT example <example@jira.com>
|
||||
To: support@example.com
|
||||
Message-ID: <18659453.58107.1576665116411@app129169.gva3.jira.com>
|
||||
Subject: [JIRA] (SYS-422) booob
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_Part_58105_29005161.1576665006397"
|
||||
X-JIRA-FingerPrint: 978a1eb52201518931e77cb5725717ff
|
||||
|
||||
------=_Part_58105_29005161.1576665006397
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_58106_4969544.1576665006398"
|
||||
|
||||
------=_Part_58106_4969544.1576665006398
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Incident content
|
Loading…
Reference in a new issue