Added postmaster filter validation. Change behaviour of using regular expressions - use explizit regular expressions by prefixing with "regex:your_regexp".

This commit is contained in:
Martin Edenhofer 2017-08-01 10:15:37 +02:00
parent d17906f333
commit 52f36f4edc
5 changed files with 546 additions and 63 deletions

View file

@ -108,7 +108,7 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
# show errors in form # show errors in form
if errors if errors
@log 'error', errors @log 'error', errors
@formValidate( form: e.target, errors: errors ) @formValidate(form: e.target, errors: errors)
return false return false
# disable form # disable form
@ -118,8 +118,10 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
object.save( object.save(
done: => done: =>
@close() @close()
fail: => fail: (settings, details) =>
@close() @log 'errors', details
@formEnable(e)
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
) )
class App.ChannelEmailSignature extends App.Controller class App.ChannelEmailSignature extends App.Controller
@ -201,7 +203,7 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
# show errors in form # show errors in form
if errors if errors
@log 'error', errors @log 'error', errors
@formValidate( form: e.target, errors: errors ) @formValidate(form: e.target, errors: errors)
return false return false
# disable form # disable form
@ -211,8 +213,10 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
object.save( object.save(
done: => done: =>
@close() @close()
fail: => fail: (settings, details) =>
@log 'errors', details
@formEnable(e) @formEnable(e)
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
) )
class App.ChannelEmailAccountOverview extends App.Controller class App.ChannelEmailAccountOverview extends App.Controller

View file

@ -6,7 +6,7 @@ class App.PostmasterFilter extends App.Model
@configure_attributes = [ @configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 250, 'null': false }, { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 250, 'null': false },
{ name: 'channel', display: 'Channel', type: 'input', readonly: 1 }, { name: 'channel', display: 'Channel', type: 'input', readonly: 1 },
{ name: 'match', display: 'Match all of the following', tag: 'postmaster_match' }, { name: 'match', display: 'Match all of the following', tag: 'postmaster_match', note: 'You can use regular expression by using "regex:your_reg_exp".' },
{ name: 'perform', display: 'Perform action of the following', tag: 'postmaster_set' }, { name: 'perform', display: 'Perform action of the following', tag: 'postmaster_set' },
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },

View file

@ -14,26 +14,27 @@ module Channel::Filter::Database
filter[:match].each { |key, meta| filter[:match].each { |key, meta|
begin begin
next if meta.blank? || meta['value'].blank? next if meta.blank? || meta['value'].blank?
value = mail[ key.downcase.to_sym ]
match_rule = meta['value']
min_one_rule_exists = true min_one_rule_exists = true
has_matched = false if meta[:operator] == 'contains not'
if mail[ key.downcase.to_sym ].present? && mail[ key.downcase.to_sym ] =~ /#{meta['value']}/i if value.present? && match(value, match_rule, false)
has_matched = true
end
if has_matched
if meta[:operator] == 'contains not'
all_matches_ok = false all_matches_ok = false
Rails.logger.info " matching #{key.downcase}:'#{value}' on #{match_rule}, but shoud not"
end
elsif meta[:operator] == 'contains'
if value.blank? || !match(value, match_rule, true)
all_matches_ok = false
Rails.logger.info " not matching #{key.downcase}:'#{value}' on #{match_rule}, but should"
end end
Rails.logger.info " matching #{key.downcase}:'#{mail[ key.downcase.to_sym ]}' on #{meta['value']}"
else else
if meta[:operator] == 'contains' all_matches_ok = false
all_matches_ok = false Rails.logger.info " Invalid operator in match #{meta.inspect}"
end
Rails.logger.info " not matching #{key.downcase}:'#{mail[ key.downcase.to_sym ]}' on #{meta['value']}"
end end
break if !all_matches_ok break if !all_matches_ok
rescue => e rescue => e
all_matches_ok = false all_matches_ok = false
Rails.logger.error "can't use match rule #{meta['value']} on #{mail[ key.to_sym ]}" Rails.logger.error "can't use match rule #{match_rule} on #{value}"
Rails.logger.error e.inspect Rails.logger.error e.inspect
end end
} }
@ -49,4 +50,31 @@ module Channel::Filter::Database
} }
end end
def self.match(value, match_rule, _should_match, check_mode = false)
regexp = false
if match_rule =~ /^(regex:)(.+?)$/
regexp = true
match_rule = $2
end
if regexp == false
match_rule_quoted = Regexp.quote(match_rule).gsub(/\\\*/, '.*')
return true if value =~ /#{match_rule_quoted}/i
return false
end
begin
return true if value =~ /#{match_rule}/i
return false
rescue => e
message = "Can't use regex '#{match_rule}' on '#{value}': #{e.message}"
Rails.logger.error message
raise message if check_mode == true
end
false
end
end end

View file

@ -4,4 +4,26 @@ class PostmasterFilter < ApplicationModel
store :perform store :perform
store :match store :match
validates :name, presence: true validates :name, presence: true
before_create :validate_condition
before_update :validate_condition
def validate_condition
raise Exceptions::UnprocessableEntity, 'Min. one match rule needed!' if match.blank?
match.each { |_key, meta|
raise Exceptions::UnprocessableEntity, 'operator invalid, ony "contains" and "contains not" is supported' if meta['operator'].blank? || meta['operator'] !~ /^(contains|contains not)$/
raise Exceptions::UnprocessableEntity, 'value invalid/empty' if meta['value'].blank?
begin
if meta['operator'] == 'contains not'
Channel::Filter::Database.match('test content', meta['value'], false, true)
else
Channel::Filter::Database.match('test content', meta['value'], true, true)
end
rescue => e
raise Exceptions::UnprocessableEntity, e.message
end
}
true
end
end end

View file

@ -3,6 +3,376 @@
require 'test_helper' require 'test_helper'
class EmailPostmasterTest < ActiveSupport::TestCase class EmailPostmasterTest < ActiveSupport::TestCase
test 'valid/invalid postmaster filter' do
PostmasterFilter.create!(
name: 'not used',
match: {
from: {
operator: 'contains',
value: 'nobody@example.com',
},
},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
assert_raises(Exceptions::UnprocessableEntity) {
PostmasterFilter.create!(
name: 'empty filter should not work',
match: {},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
}
assert_raises(Exceptions::UnprocessableEntity) {
PostmasterFilter.create!(
name: 'empty filter should not work',
match: {
from: {
operator: 'contains',
value: '',
},
},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
}
assert_raises(Exceptions::UnprocessableEntity) {
PostmasterFilter.create!(
name: 'invalid regex',
match: {
from: {
operator: 'contains',
value: 'regex:[]',
},
},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
}
assert_raises(Exceptions::UnprocessableEntity) {
PostmasterFilter.create!(
name: 'invalid regex',
match: {
from: {
operator: 'contains',
value: 'regex:??',
},
},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
}
assert_raises(Exceptions::UnprocessableEntity) {
PostmasterFilter.create!(
name: 'invalid regex',
match: {
from: {
operator: 'contains',
value: 'regex:*',
},
},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
}
PostmasterFilter.create!(
name: 'use .*',
match: {
from: {
operator: 'contains',
value: '*',
},
},
perform: {
'X-Zammad-Ticket-priority' => {
value: '3 high',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
end
test 'process with postmaster filter with regex' do
group_default = Group.lookup(name: 'Users')
group1 = Group.create_if_not_exists(
name: 'Test Group1',
created_by_id: 1,
updated_by_id: 1,
)
group2 = Group.create_if_not_exists(
name: 'Test Group2',
created_by_id: 1,
updated_by_id: 1,
)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'used - empty selector',
match: {
from: {
operator: 'contains',
value: 'regex:.*',
},
},
perform: {
'X-Zammad-Ticket-group_id' => {
value: group2.id,
},
'X-Zammad-Ticket-priority_id' => {
value: '1',
},
'x-Zammad-Article-Internal' => {
value: true,
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: Some Body <somebody@example.com>
To: Bob <bod@example.com>
Cc: any@example.com
Subject: some subject - no selector
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Test Group2', ticket.group.name)
assert_equal('1 low', ticket.priority.name)
assert_equal('some subject - no selector', ticket.title)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(true, article.internal)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'used - empty selector',
match: {
from: {
operator: 'contains',
value: '*',
},
},
perform: {
'X-Zammad-Ticket-group_id' => {
value: group2.id,
},
'X-Zammad-Ticket-priority_id' => {
value: '1',
},
'x-Zammad-Article-Internal' => {
value: true,
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: Some Body <somebody@example.com>
To: Bob <bod@example.com>
Cc: any@example.com
Subject: some subject - no selector
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Test Group2', ticket.group.name)
assert_equal('1 low', ticket.priority.name)
assert_equal('some subject - no selector', ticket.title)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(true, article.internal)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'used - empty selector',
match: {
subject: {
operator: 'contains',
value: '*me*',
},
},
perform: {
'X-Zammad-Ticket-group_id' => {
value: group2.id,
},
'X-Zammad-Ticket-priority_id' => {
value: '1',
},
'x-Zammad-Article-Internal' => {
value: true,
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: Some Body <somebody@example.com>
To: Bob <bod@example.com>
Cc: any@example.com
Subject: *me*
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Test Group2', ticket.group.name)
assert_equal('1 low', ticket.priority.name)
assert_equal('*me*', ticket.title)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(true, article.internal)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'used - empty selector',
match: {
subject: {
operator: 'contains not',
value: '*me*',
},
},
perform: {
'X-Zammad-Ticket-group_id' => {
value: group2.id,
},
'X-Zammad-Ticket-priority_id' => {
value: '1',
},
'x-Zammad-Article-Internal' => {
value: true,
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: Some Body <somebody@example.com>
To: Bob <bod@example.com>
Cc: any@example.com
Subject: *mo*
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Test Group2', ticket.group.name)
assert_equal('1 low', ticket.priority.name)
assert_equal('*mo*', ticket.title)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(true, article.internal)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'used - empty selector',
match: {
subject: {
operator: 'contains not',
value: '*me*',
},
},
perform: {
'X-Zammad-Ticket-group_id' => {
value: group2.id,
},
'X-Zammad-Ticket-priority_id' => {
value: '1',
},
'x-Zammad-Article-Internal' => {
value: true,
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: Some Body <somebody@example.com>
To: Bob <bod@example.com>
Cc: any@example.com
Subject: *me*
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Users', ticket.group.name)
assert_equal('2 normal', ticket.priority.name)
assert_equal('*me*', ticket.title)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(false, article.internal)
PostmasterFilter.destroy_all
end
test 'process with postmaster filter' do test 'process with postmaster filter' do
group_default = Group.lookup(name: 'Users') group_default = Group.lookup(name: 'Users')
group1 = Group.create_if_not_exists( group1 = Group.create_if_not_exists(
@ -159,51 +529,6 @@ Some Text'
PostmasterFilter.destroy_all PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'used - empty selector',
match: {
from: {
operator: 'contains',
value: '',
},
},
perform: {
'X-Zammad-Ticket-group_id' => {
value: group2.id,
},
'X-Zammad-Ticket-priority_id' => {
value: '1',
},
'x-Zammad-Article-Internal' => {
value: true,
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: Some Body <somebody@example.com>
To: Bob <bod@example.com>
Cc: any@example.com
Subject: some subject - no selector
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Users', ticket.group.name)
assert_equal('2 normal', ticket.priority.name)
assert_equal('some subject - no selector', ticket.title)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(false, article.internal)
PostmasterFilter.destroy_all
# follow up with create post master filter test # follow up with create post master filter test
PostmasterFilter.create!( PostmasterFilter.create!(
name: 'used - empty selector', name: 'used - empty selector',
@ -477,6 +802,110 @@ Some Text'
To: customer@example.com To: customer@example.com
Subject: some subject Subject: some subject
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Users', ticket.group.name)
assert_equal('2 normal', ticket.priority.name)
assert_equal('some subject', ticket.title)
assert_equal('me@example.com', ticket.customer.email)
assert_equal('2 normal', ticket.priority.name)
assert_equal('Customer', article.sender.name)
assert_equal('email', article.type.name)
assert_equal(false, article.internal)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'Autoresponder',
match: {
'auto-submitted' => {
'operator' => 'contains not',
'value' => 'auto-generated',
},
'to' => {
'operator' => 'contains',
'value' => 'customer@example.com',
},
'from' => {
'operator' => 'contains',
'value' => '@example.com',
}
},
perform: {
'x-zammad-article-internal' => {
'value' => 'true',
},
'x-zammad-article-type_id' => {
'value' => Ticket::Article::Type.find_by(name: 'note').id.to_s,
},
'x-zammad-ignore' => {
'value' => 'false',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: ME Bob <me@example.com>
To: customer@example.com
Subject: some subject
Some Text'
parser = Channel::EmailParser.new
ticket, article, user = parser.process({ group_id: group_default.id, trusted: false }, data)
assert_equal('Users', ticket.group.name)
assert_equal('2 normal', ticket.priority.name)
assert_equal('some subject', ticket.title)
assert_equal('me@example.com', ticket.customer.email)
assert_equal('2 normal', ticket.priority.name)
assert_equal('Customer', article.sender.name)
assert_equal('note', article.type.name)
assert_equal(true, article.internal)
PostmasterFilter.destroy_all
PostmasterFilter.create!(
name: 'Autoresponder',
match: {
'auto-submitted' => {
'operator' => 'contains',
'value' => 'auto-generated',
},
'to' => {
'operator' => 'contains',
'value' => 'customer1@example.com',
},
'from' => {
'operator' => 'contains',
'value' => '@example.com',
}
},
perform: {
'x-zammad-article-internal' => {
'value' => 'true',
},
'x-zammad-article-type_id' => {
'value' => Ticket::Article::Type.find_by(name: 'note').id.to_s,
},
'x-zammad-ignore' => {
'value' => 'false',
},
},
channel: 'email',
active: true,
created_by_id: 1,
updated_by_id: 1,
)
data = 'From: ME Bob <me@example.com>
To: customer@example.com
Subject: some subject
Some Text' Some Text'
parser = Channel::EmailParser.new parser = Channel::EmailParser.new