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/
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
module Channel::Filter::ServiceNowCheck
|
class Channel::Filter::ServiceNowCheck < Channel::Filter::BaseExternalCheck
|
||||||
|
MAIL_HEADER = 'x-servicenow-generated'.freeze
|
||||||
# This filter will run pre and post
|
SOURCE_ID_REGEX = %r{\s(INC\d+)\s}.freeze
|
||||||
def self.run(_channel, mail, ticket_or_transaction_params = nil, _article = nil, _session_user = nil)
|
SOURCE_NAME_PREFIX = 'ServiceNow'.freeze
|
||||||
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
|
|
||||||
end
|
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',
|
state: 'Channel::Filter::ServiceNowCheck',
|
||||||
frontend: false
|
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(
|
Setting.create_if_not_exists(
|
||||||
title: 'Define postmaster filter.',
|
title: 'Define postmaster filter.',
|
||||||
name: '5500_postmaster_internal_article_check',
|
name: '5500_postmaster_internal_article_check',
|
||||||
|
|
|
@ -1169,6 +1169,50 @@ RSpec.describe Channel::EmailParser, type: :model do
|
||||||
end
|
end
|
||||||
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
|
describe 'XSS protection' do
|
||||||
let(:article) { described_class.new.process({}, raw_mail).second }
|
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