mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
Fixes #5815 - Allow usage of extracted text from regex
This commit is contained in:
parent
d09a993145
commit
332b0d8c72
14 changed files with 510 additions and 321 deletions
|
|
@ -263,7 +263,6 @@ Metrics/CyclomaticComplexity:
|
|||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
- 'app/models/channel/filter/bounce_delivery_permanent_failed.rb'
|
||||
- 'app/models/channel/filter/database.rb'
|
||||
- 'app/models/channel/filter/follow_up_check.rb'
|
||||
- 'app/models/channel/filter/identify_sender.rb'
|
||||
- 'app/models/channel/filter/monitoring_base.rb'
|
||||
|
|
@ -299,6 +298,7 @@ Metrics/CyclomaticComplexity:
|
|||
- 'lib/external_credential/microsoft_base.rb'
|
||||
- 'lib/facebook.rb'
|
||||
- 'lib/fill_db.rb'
|
||||
- 'lib/filter_processor.rb'
|
||||
- 'lib/models.rb'
|
||||
- 'lib/notification_factory/mailer.rb'
|
||||
- 'lib/notification_factory/renderer.rb'
|
||||
|
|
|
|||
|
|
@ -1,141 +1,11 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# process all database filter
|
||||
module Channel::Filter::Database # rubocop:disable Metrics/ModuleLength
|
||||
|
||||
OPERATORS_WITH_MULTIPLE_VALUES = [
|
||||
'is any of',
|
||||
'is none of',
|
||||
'starts with one of',
|
||||
'ends with one of',
|
||||
].freeze
|
||||
|
||||
module Channel::Filter::Database
|
||||
def self.run(_channel, mail, _transaction_params)
|
||||
PostmasterFilter.where(active: true, channel: 'email').reorder(:name, :created_at).each do |filter|
|
||||
Rails.logger.debug { " process filter #{filter.name} ..." }
|
||||
perform_filter_changes(mail, filter) if filter_matches?(mail, filter)
|
||||
FilterProcessor.new(filter, mail).process
|
||||
end
|
||||
end
|
||||
|
||||
def self.filter_matches?(mail, filter)
|
||||
|
||||
min_one_rule_exists = false
|
||||
|
||||
filter[:match].each do |key, meta|
|
||||
|
||||
next if meta.blank? || meta['value'].blank?
|
||||
|
||||
value = mail[ key.downcase.to_sym ]
|
||||
match_rule = meta['value']
|
||||
min_one_rule_exists = true
|
||||
operator = meta[:operator]
|
||||
|
||||
human_match_rule = match_rule
|
||||
|
||||
if OPERATORS_WITH_MULTIPLE_VALUES.include?(operator) && !match_rule.instance_of?(Array)
|
||||
match_rule = [match_rule]
|
||||
human_match_rule = match_rule.join(', ')
|
||||
end
|
||||
|
||||
if !rule_matches?(operator, match_rule, value)
|
||||
Rails.logger.debug { " not matching: key '#{key.downcase}' #{operator} '#{human_match_rule}'" }
|
||||
return false
|
||||
end
|
||||
|
||||
Rails.logger.info { " matching: key '#{key.downcase}' #{operator} '#{human_match_rule}'" }
|
||||
rescue => e
|
||||
Rails.logger.error "can't use match rule '#{human_match_rule}' on '#{value}'"
|
||||
Rails.logger.error e.inspect
|
||||
return false
|
||||
end
|
||||
|
||||
min_one_rule_exists
|
||||
end
|
||||
|
||||
def self.rule_matches?(operator, match_rule, value)
|
||||
case operator
|
||||
when 'contains not'
|
||||
value.blank? || !Channel::Filter::Match::Contains.match(value: value, match_rule: match_rule)
|
||||
when 'contains'
|
||||
value.present? && Channel::Filter::Match::Contains.match(value: value, match_rule: match_rule)
|
||||
when 'is any of'
|
||||
match_rule.any?(value)
|
||||
when 'is none of'
|
||||
match_rule.none?(value)
|
||||
when 'starts with one of'
|
||||
match_rule.any? { |rule_value| value.downcase.start_with? rule_value.downcase }
|
||||
when 'ends with one of'
|
||||
match_rule.any? { |rule_value| value.downcase.end_with? rule_value.downcase }
|
||||
when 'matches regex'
|
||||
value.present? && Channel::Filter::Match::EmailRegex.match(value: value, match_rule: match_rule)
|
||||
when 'does not match regex'
|
||||
value.blank? || !Channel::Filter::Match::EmailRegex.match(value: value, match_rule: match_rule)
|
||||
else
|
||||
Rails.logger.info { " Invalid operator in match #{meta.inspect}" }
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def self.perform_filter_changes(mail, filter)
|
||||
filter[:perform].each do |key, meta|
|
||||
next if !Channel::EmailParser.check_attributes_by_x_headers(key, meta['value'])
|
||||
|
||||
Rails.logger.debug { " perform '#{key.downcase}' = '#{meta.inspect}'" }
|
||||
|
||||
next if perform_filter_changes_tags(mail: mail, filter: filter, key: key, meta: meta)
|
||||
next if perform_filter_changes_date(mail: mail, filter: filter, key: key, meta: meta)
|
||||
|
||||
mail[ key.downcase.to_sym ] = meta['value']
|
||||
mail[:"#{key.downcase}-source"] = filter
|
||||
end
|
||||
end
|
||||
|
||||
def self.perform_filter_changes_tags(mail:, filter:, key:, meta:)
|
||||
return if %w[x-zammad-ticket-tags x-zammad-ticket-followup-tags].exclude?(key.downcase)
|
||||
|
||||
mail_header_key = key.downcase.to_sym
|
||||
current_tags = mail[mail_header_key].to_s.split(',').map(&:strip).compact_blank
|
||||
change_tags = meta['value'].split(',').map(&:strip).compact_blank
|
||||
|
||||
case meta['operator']
|
||||
when 'add'
|
||||
change_tags.each do |tag|
|
||||
current_tags |= [tag]
|
||||
mail[:"#{key.downcase}-source"] = filter
|
||||
end
|
||||
when 'remove'
|
||||
change_tags.each do |tag|
|
||||
current_tags -= [tag]
|
||||
mail[:"#{key.downcase}-source"] = filter
|
||||
end
|
||||
end
|
||||
|
||||
mail[mail_header_key] = current_tags.join(',')
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def self.perform_filter_changes_date(mail:, filter:, key:, meta:)
|
||||
return if key !~ %r{x-zammad-ticket-(?:followup-)?(.*)}
|
||||
|
||||
object_attribute = ObjectManager::Attribute.for_object('Ticket').find_by(name: $1, data_type: %w[datetime date])
|
||||
return if object_attribute.blank?
|
||||
|
||||
new_value = if meta['operator'] == 'relative'
|
||||
TimeRangeHelper.relative(range: meta['range'], value: meta['value'])
|
||||
else
|
||||
meta['value']
|
||||
end
|
||||
|
||||
if new_value
|
||||
mail[ key.downcase.to_sym ] = if object_attribute[:data_type] == 'datetime'
|
||||
new_value.to_datetime
|
||||
else
|
||||
new_value.to_date
|
||||
end
|
||||
mail[:"#{key.downcase}-source"] = filter
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Channel::Filter::Match::Contains
|
||||
|
||||
def self.match(value:, match_rule:)
|
||||
match_rule_quoted = Regexp.quote(match_rule).gsub(%r{\\\*}, '.*')
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
module Channel::Filter::Match::EmailRegex
|
||||
def self.match(value:, match_rule:, check_mode: false)
|
||||
begin
|
||||
return value.match?(%r{#{match_rule}}i)
|
||||
return value.match(%r{#{match_rule}}i)
|
||||
rescue => e
|
||||
message = "Can't use regex '#{match_rule}' on '#{value}': #{e.message}"
|
||||
Rails.logger.error message
|
||||
|
|
|
|||
7
app/models/channel/filter/match/ends_with.rb
Normal file
7
app/models/channel/filter/match/ends_with.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Channel::Filter::Match::EndsWith
|
||||
def self.match(value:, match_rule:)
|
||||
match_rule.any? { |rule_value| value.downcase.end_with? rule_value.downcase }
|
||||
end
|
||||
end
|
||||
7
app/models/channel/filter/match/is_any_of.rb
Normal file
7
app/models/channel/filter/match/is_any_of.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Channel::Filter::Match::IsAnyOf
|
||||
def self.match(value:, match_rule:)
|
||||
match_rule.any?(value)
|
||||
end
|
||||
end
|
||||
7
app/models/channel/filter/match/starts_with.rb
Normal file
7
app/models/channel/filter/match/starts_with.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Channel::Filter::Match::StartsWith
|
||||
def self.match(value:, match_rule:)
|
||||
match_rule.any? { |rule_value| value.downcase.start_with?(rule_value.downcase) }
|
||||
end
|
||||
end
|
||||
171
lib/filter_processor.rb
Normal file
171
lib/filter_processor.rb
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class FilterProcessor
|
||||
OPERATORS_WITH_MULTIPLE_VALUES = [
|
||||
'is any of',
|
||||
'is none of',
|
||||
'starts with one of',
|
||||
'ends with one of',
|
||||
].freeze
|
||||
|
||||
attr_reader :filter, :mail, :match_data
|
||||
|
||||
def initialize(filter, mail)
|
||||
@filter = filter
|
||||
@mail = mail
|
||||
@match_data = {}
|
||||
end
|
||||
|
||||
def process
|
||||
return if !filter_matches?
|
||||
|
||||
perform_filter_changes
|
||||
end
|
||||
|
||||
def filter_matches?
|
||||
|
||||
min_one_rule_exists = false
|
||||
|
||||
@filter[:match].each do |key, meta|
|
||||
|
||||
next if meta.blank? || meta['value'].blank?
|
||||
|
||||
value = @mail[ key.downcase.to_sym ]
|
||||
match_rule = meta['value']
|
||||
min_one_rule_exists = true
|
||||
operator = meta[:operator]
|
||||
|
||||
human_match_rule = match_rule
|
||||
|
||||
if OPERATORS_WITH_MULTIPLE_VALUES.include?(operator) && !match_rule.instance_of?(Array)
|
||||
match_rule = [match_rule]
|
||||
human_match_rule = match_rule.join(', ')
|
||||
end
|
||||
|
||||
if !rule_matches?(operator, match_rule, value)
|
||||
Rails.logger.debug { " not matching: key '#{key.downcase}' #{operator} '#{human_match_rule}'" }
|
||||
return false
|
||||
end
|
||||
|
||||
Rails.logger.info { " matching: key '#{key.downcase}' #{operator} '#{human_match_rule}'" }
|
||||
rescue => e
|
||||
Rails.logger.error "can't use match rule '#{human_match_rule}' on '#{value}'"
|
||||
Rails.logger.error e.inspect
|
||||
return false
|
||||
end
|
||||
|
||||
min_one_rule_exists
|
||||
end
|
||||
|
||||
def rule_matches?(operator, match_rule, value)
|
||||
case operator
|
||||
when 'contains not'
|
||||
value.blank? || !Channel::Filter::Match::Contains.match(value: value, match_rule: match_rule)
|
||||
when 'contains'
|
||||
value.present? && Channel::Filter::Match::Contains.match(value: value, match_rule: match_rule)
|
||||
when 'is any of'
|
||||
Channel::Filter::Match::IsAnyOf.match(value: value, match_rule: match_rule)
|
||||
when 'is none of'
|
||||
!Channel::Filter::Match::IsAnyOf.match(value: value, match_rule: match_rule)
|
||||
when 'starts with one of'
|
||||
Channel::Filter::Match::StartsWith.match(value: value, match_rule: match_rule)
|
||||
when 'ends with one of'
|
||||
Channel::Filter::Match::EndsWith.match(value: value, match_rule: match_rule)
|
||||
when 'matches regex'
|
||||
return false if value.blank?
|
||||
|
||||
match_data = Channel::Filter::Match::EmailRegex.match(value: value, match_rule: match_rule)
|
||||
if match_data.respond_to?(:names)
|
||||
match_data.names.each do |key|
|
||||
@match_data[key] = match_data[key]
|
||||
end
|
||||
end
|
||||
match_data
|
||||
when 'does not match regex'
|
||||
value.blank? || !Channel::Filter::Match::EmailRegex.match(value: value, match_rule: match_rule)
|
||||
else
|
||||
Rails.logger.info { " Invalid operator in match #{meta.inspect}" }
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def perform_filter_changes
|
||||
@filter[:perform].each do |key, meta|
|
||||
next if !Channel::EmailParser.check_attributes_by_x_headers(key, meta['value'])
|
||||
|
||||
Rails.logger.debug { " perform '#{key.downcase}' = '#{meta.inspect}'" }
|
||||
|
||||
next if perform_filter_changes_tags(key: key, meta: meta)
|
||||
next if perform_filter_changes_date(key: key, meta: meta)
|
||||
|
||||
perform_filter_changes_regular(key: key, meta: meta)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def perform_filter_changes_general(key:, meta:)
|
||||
@mail[ key.downcase.to_sym ] = meta['value']
|
||||
@mail[:"#{key.downcase}-source"] = @filter
|
||||
end
|
||||
|
||||
def perform_filter_changes_tags(key:, meta:)
|
||||
return if %w[x-zammad-ticket-tags x-zammad-ticket-followup-tags].exclude?(key.downcase)
|
||||
|
||||
mail_header_key = key.downcase.to_sym
|
||||
current_tags = @mail[mail_header_key].to_s.split(',').map(&:strip).compact_blank
|
||||
change_tags = meta['value'].split(',').map(&:strip).compact_blank
|
||||
|
||||
case meta['operator']
|
||||
when 'add'
|
||||
change_tags.each do |tag|
|
||||
current_tags |= [tag]
|
||||
@mail[:"#{key.downcase}-source"] = @filter
|
||||
end
|
||||
when 'remove'
|
||||
change_tags.each do |tag|
|
||||
current_tags -= [tag]
|
||||
@mail[:"#{key.downcase}-source"] = @filter
|
||||
end
|
||||
end
|
||||
|
||||
@mail[mail_header_key] = current_tags.join(',')
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def perform_filter_changes_regular(key:, meta:)
|
||||
value = meta['value']
|
||||
if value.respond_to?(:gsub)
|
||||
value.gsub!(%r{#\{(.+?)\}}) do
|
||||
@match_data[$1] || ''
|
||||
end
|
||||
end
|
||||
|
||||
@mail[ key.downcase.to_sym ] = value
|
||||
@mail[:"#{key.downcase}-source"] = @filter
|
||||
end
|
||||
|
||||
def perform_filter_changes_date(key:, meta:)
|
||||
return if key !~ %r{x-zammad-ticket-(?:followup-)?(.*)}
|
||||
|
||||
object_attribute = ObjectManager::Attribute.for_object('Ticket').find_by(name: $1, data_type: %w[datetime date])
|
||||
return if object_attribute.blank?
|
||||
|
||||
new_value = if meta['operator'] == 'relative'
|
||||
TimeRangeHelper.relative(range: meta['range'], value: meta['value'])
|
||||
else
|
||||
meta['value']
|
||||
end
|
||||
|
||||
if new_value
|
||||
@mail[ key.downcase.to_sym ] = if object_attribute[:data_type] == 'datetime'
|
||||
new_value.to_datetime
|
||||
else
|
||||
new_value.to_date
|
||||
end
|
||||
@mail[:"#{key.downcase}-source"] = @filter
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
193
spec/lib/filter_processor_spec.rb
Normal file
193
spec/lib/filter_processor_spec.rb
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FilterProcessor, type: :channel_filter do
|
||||
let(:mail_hash) { Channel::EmailParser.new.parse(<<~RAW.chomp) }
|
||||
From: daffy.duck@acme.corp
|
||||
To: batman@marvell.com
|
||||
Subject: Anvil
|
||||
|
||||
I can haz anvil!
|
||||
RAW
|
||||
|
||||
describe '.filter_matches?' do
|
||||
let(:filter) { create(:postmaster_filter, match: { 'from' => { 'operator' => operator, 'value' => value } }) }
|
||||
|
||||
shared_examples 'the filter matches' do
|
||||
it 'matches' do
|
||||
expect(described_class.new(filter, mail_hash).filter_matches?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'the filter does not match' do
|
||||
it 'matches' do
|
||||
expect(described_class.new(filter, mail_hash).filter_matches?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'contains'" do
|
||||
let(:operator) { 'contains' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'a' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { 'A' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'x' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'contains not'" do
|
||||
let(:operator) { 'contains not' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'a' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { 'A' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'x' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'matches regex'" do
|
||||
let(:operator) { 'matches regex' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'daffy.duck@.*' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'daffy.duck.+@' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'does not match regex'" do
|
||||
let(:operator) { 'does not match regex' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'daffy.duck@.*' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'daffy.duck.+@' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'is any of'" do
|
||||
let(:operator) { 'is any of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['daffy.duck@acme.corp', 'elmer.fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['Daffy.Duck@acme.corp', 'Elmer.Fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['other.address@example.com', 'mail@example.com'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'is none of'" do
|
||||
let(:operator) { 'is none of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['daffy.duck@acme.corp', 'elmer.fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['Daffy.Duck@acme.corp', 'Elmer.Fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['other.address@example.com', 'mail@example.com'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'starts with one of'" do
|
||||
let(:operator) { 'starts with one of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['daffy.duck', 'elmer.fudd'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['Daffy.Duck', 'Elmer.Fudd'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['other.address', 'zammad.org'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'ends with one of'" do
|
||||
let(:operator) { 'ends with one of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['acme.corp', 'example.com'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['ACME.corp', 'EXAMPLE.com'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['example.com', 'zammad.org'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,186 +11,6 @@ RSpec.describe Channel::Filter::Database, type: :channel_filter do
|
|||
I can haz anvil!
|
||||
RAW
|
||||
|
||||
describe '.filter_matches?' do
|
||||
let(:filter) { create(:postmaster_filter, match: { 'from' => { 'operator' => operator, 'value' => value } }) }
|
||||
|
||||
shared_examples 'the filter matches' do
|
||||
it 'matches' do
|
||||
expect(described_class.filter_matches?(mail_hash, filter)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'the filter does not match' do
|
||||
it 'matches' do
|
||||
expect(described_class.filter_matches?(mail_hash, filter)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'contains'" do
|
||||
let(:operator) { 'contains' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'a' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { 'A' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'x' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'contains not'" do
|
||||
let(:operator) { 'contains not' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'a' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { 'A' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'x' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'matches regex'" do
|
||||
let(:operator) { 'matches regex' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'daffy.duck@.*' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'daffy.duck.+@' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'does not match regex'" do
|
||||
let(:operator) { 'does not match regex' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { 'daffy.duck@.*' }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { 'daffy.duck.+@' }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'is any of'" do
|
||||
let(:operator) { 'is any of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['daffy.duck@acme.corp', 'elmer.fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['Daffy.Duck@acme.corp', 'Elmer.Fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['other.address@example.com', 'mail@example.com'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'is none of'" do
|
||||
let(:operator) { 'is none of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['daffy.duck@acme.corp', 'elmer.fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['Daffy.Duck@acme.corp', 'Elmer.Fudd@acme.corp'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['other.address@example.com', 'mail@example.com'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'starts with one of'" do
|
||||
let(:operator) { 'starts with one of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['daffy.duck', 'elmer.fudd'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['Daffy.Duck', 'Elmer.Fudd'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['other.address', 'zammad.org'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
|
||||
context "with operator 'ends with one of'" do
|
||||
let(:operator) { 'ends with one of' }
|
||||
|
||||
context 'with matching string' do
|
||||
let(:value) { ['acme.corp', 'example.com'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with matching upcased string' do
|
||||
let(:value) { ['ACME.corp', 'EXAMPLE.com'] }
|
||||
|
||||
include_examples 'the filter matches'
|
||||
end
|
||||
|
||||
context 'with non-matching string' do
|
||||
let(:value) { ['example.com', 'zammad.org'] }
|
||||
|
||||
include_examples 'the filter does not match'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Cannot set date for pending close status in postmaster filter #4206', db_strategy: :reset do
|
||||
before do
|
||||
freeze_time
|
||||
|
|
@ -360,4 +180,32 @@ RSpec.describe Channel::Filter::Database, type: :channel_filter do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'postmaster filter with regex and named capture', db_strategy: :reset do
|
||||
context 'when the filter uses a named capture group' do
|
||||
let(:mail_hash) { Channel::EmailParser.new.parse(<<~RAW.chomp) }
|
||||
From: daffy.duck@acme.corp
|
||||
To: batman@marvell.com
|
||||
Subject: Anvil
|
||||
|
||||
Customers Formular Message is:
|
||||
|
||||
ContractID: 1234abcd
|
||||
CustomerEmail: customer@example.com
|
||||
RAW
|
||||
|
||||
before do
|
||||
create(:postmaster_filter,
|
||||
match: { 'body' => { 'operator' => 'matches regex', 'value' => 'ContractID:\s(?<CONTRACT_ID>.*)' } },
|
||||
perform: { 'x-zammad-ticket-title' => { 'value' => '#{CONTRACT_ID}' } }, # rubocop:disable Lint/InterpolationCheck
|
||||
channel: 'email',
|
||||
active: true)
|
||||
end
|
||||
|
||||
it 'sets the ticket title using the captured CONTRACT_ID' do
|
||||
described_class.run({}, mail_hash, {})
|
||||
expect(mail_hash[:'x-zammad-ticket-title']).to eq('1234abcd')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@ RSpec.describe Channel::Filter::Match::EmailRegex do
|
|||
context 'with empty string' do
|
||||
let(:sender) { '' }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
it { is_expected.to be_a(MatchData) }
|
||||
end
|
||||
|
||||
context 'and matching regex' do
|
||||
let(:sender) { 'foobar@.*' }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
it { is_expected.to be_a(MatchData) }
|
||||
end
|
||||
|
||||
context 'and non-matching regex' do
|
||||
let(:sender) { 'nagios@.*' }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'and invalid regex (misused ? repeat operator)' do
|
||||
|
|
@ -54,19 +54,19 @@ RSpec.describe Channel::Filter::Match::EmailRegex do
|
|||
context 'with empty string' do
|
||||
let(:sender) { '' }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
it { is_expected.to be_a(MatchData) }
|
||||
end
|
||||
|
||||
context 'and matching regex' do
|
||||
let(:sender) { 'foobar@.*' }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
it { is_expected.to be_a(MatchData) }
|
||||
end
|
||||
|
||||
context 'and non-matching regex' do
|
||||
let(:sender) { 'nagios@.*' }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'and invalid regex (misused ? repeat operator)' do
|
||||
|
|
|
|||
29
spec/models/channel/filter/match/ends_with_spec.rb
Normal file
29
spec/models/channel/filter/match/ends_with_spec.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channel::Filter::Match::EndsWith do
|
||||
describe '.match' do
|
||||
subject(:match) { described_class.match(value: from, match_rule: rules) }
|
||||
|
||||
let(:from) { 'foobar@foo.bar' }
|
||||
|
||||
context 'when the value ends with a matching rule' do
|
||||
let(:rules) { ['bar'] }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'with a correct beginning but upper letter matching single rule' do
|
||||
let(:rules) { ['Bar'] }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'when the value does not end with any rule' do
|
||||
let(:rules) { ['far'] }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
29
spec/models/channel/filter/match/is_any_of_spec.rb
Normal file
29
spec/models/channel/filter/match/is_any_of_spec.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channel::Filter::Match::IsAnyOf do
|
||||
describe '.match' do
|
||||
subject(:match) { described_class.match(value: subject_value, match_rule: subject_rule) }
|
||||
|
||||
let(:subject_value) { 'Subject1' }
|
||||
|
||||
context 'when the rule includes the exact same subject' do
|
||||
let(:subject_rule) { %w[Subject1 Subject2] }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'when the rule does not include the subject' do
|
||||
let(:subject_rule) { %w[Subject01 Subject02] }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
|
||||
context 'when the rule includes a case-insensitive match' do
|
||||
let(:subject_rule) { %w[subject1 subject2] }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
29
spec/models/channel/filter/match/starts_with_spec.rb
Normal file
29
spec/models/channel/filter/match/starts_with_spec.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channel::Filter::Match::StartsWith do
|
||||
describe '.match' do
|
||||
subject(:match) { described_class.match(value: from, match_rule: rules) }
|
||||
|
||||
let(:from) { 'foobar@foo.bar' }
|
||||
|
||||
context 'when the value starts with a matching rule' do
|
||||
let(:rules) { ['foo'] }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'when the value starts with a matching rule in a different case' do
|
||||
let(:rules) { ['Foo'] }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'when the value does not start with any rule' do
|
||||
let(:rules) { ['doo'] }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue