Fixes #3695 - Detect Jira follow-ups.

This commit is contained in:
Rolf Schmidt 2021-08-13 14:39:09 +02:00 committed by Thorsten Eckel
parent 86e18abd00
commit 7e6e751d69
8 changed files with 233 additions and 82 deletions

View 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

View 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

View file

@ -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

View 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

View file

@ -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',

View file

@ -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 }

View 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

View 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