diff --git a/app/mailboxes/in_mailbox.rb b/app/mailboxes/in_mailbox.rb index 5d1558e..e35fc0e 100644 --- a/app/mailboxes/in_mailbox.rb +++ b/app/mailboxes/in_mailbox.rb @@ -2,11 +2,11 @@ class InMailbox < ApplicationMailbox def process email_importer = EmailImporter.new repository = EmailRepository.new - rules_evaluator = RulesEvaluator.new email = email_importer.import(mail) - email = rules_evaluator.evaluate(email, RuleSet.enabled) + processor = RuleSetProcessor.new(email: email) + email = processor.process_all(RuleSet.enabled) repository.save(email) end diff --git a/app/services/email_action/base.rb b/app/services/email_action/base.rb index 168ba2d..11d8099 100644 --- a/app/services/email_action/base.rb +++ b/app/services/email_action/base.rb @@ -10,6 +10,7 @@ module EmailAction def process(email) fail NotImplementedError end + end end diff --git a/app/services/rule_set_processor.rb b/app/services/rule_set_processor.rb new file mode 100644 index 0000000..5444d11 --- /dev/null +++ b/app/services/rule_set_processor.rb @@ -0,0 +1,137 @@ +class RuleSetProcessor + + class InvalidRule < ::ArgumentError + end + + def process_all(rule_sets, email) + rule_sets.each { |rule_set| + email = process(rule_set, email) + } + + email + end + + def process(rule_set, email) + return email unless rule_set.enabled? + + if evaluate_rules(rule_set, email) + email = execute_actions(rule_set.actions, email) + end + + email + end + + def evaluate_rules(rule_set, email) + rule_set_result = true + + rule_set_result = catch(:done) { + rule_set.rules.each do |rule| + next unless rule.enabled? + + subjects = prepare_subjects(rule, email) + rule_result = apply_rule(subjects, rule) + rule_result = !rule_result if rule.inverted? + rule_set_result = apply_operator(rule_set_result, rule_set.operator, rule_result) + rescue InvalidRule => ex + Rails.logger.error "Skipped rule##{rule.id} '#{rule.name}' - #{ex.inspect}" + next + end + } + + rule_set_result + end + + def execute_actions(actions, email) + actions.each do |action| + next unless action.enabled? + + klass = action.class_name.constantize + email_action = klass.new + email = email_action.process(email) + rescue NameError => ex + Rails.logger.error "Skipped action##{action.id} '#{action.name}' - #{ex.inspect}" + raise InvalidRule, ex.message + end + + email + end + + def prepare_subjects(rule, email) + case rule.subject_type.downcase + when "header" + Array(email.header_values(rule.subject_value)) + when "body" + Array(email.plain_body) + else + raise InvalidRule, "Unrecognized subject type '#{rule.subject_type}'" + end + end + + private + + def apply_rule(subjects, rule) + case rule.condition_type.downcase + when "match", "matches" + subjects.any? { |subject| + pattern = Regexp.new(rule.condition_value) + subject.match? + } + when "equal", "equals" + subjects.any? { |subject| + subject == rule.condition_value + } + when "start", "starts" + subjects.any? { |subject| + subject.starts_with? rule.condition_value + } + when "end", "ends" + subjects.any? { |subject| + subject.ends_with? rule.condition_value + } + when "contain", "contains" + subjects.any? { |subject| + subject.include? rule.condition_value + } + when "exist", "exists" + subjects.any? { |subject| + subject.exists? + } + when "empty" + subjects.all? { |subject| + subject.empty? + } + when "date_before" + # subjects.all? { |subject| + # subject.empty? + # } + when "date_after" + # subjects.all? { |subject| + # subject.empty? + # } + else + raise InvalidRule, "Unrecognized condition type '#{rule. condition_type}'" + end + end + + def apply_operator(state, operator, result) + case operator.upcase + when "AND" + if result + (state and result) + else + throw :done, false + end + when "OR" + if result + throw :done, true + else + (state or result) + end + # when "XOR" + # (state or result) and !(rules_state and result) + else + raise InvalidRule, "Unrecognized operator '#{operator}'" + end + end + +end diff --git a/app/services/rules_evaluator.rb b/app/services/rules_evaluator.rb deleted file mode 100644 index f39a21a..0000000 --- a/app/services/rules_evaluator.rb +++ /dev/null @@ -1,110 +0,0 @@ -class RulesEvaluator - - def evaluate(email, rule_set_or_sets) - if rule_set_or_sets.respond_to? :each - rule_set_or_sets.each do |rule_set| - email = evaluate_rule_set(email, rule_set) - end - else - email = evaluate_rule_set(email, rule_set_or_sets) - end - - email - end - - private - - def evaluate_rule_set(email, rule_set) - rules_state = true - - rule_set.rules.enabled.each do |rule| - subjects = prepare_subjects(email, rule) - result = apply_rule(subjects, rule) - result = ! result if rule.inverted? - rules_state = apply_operator(rules_state, rule_set.operator, result) - - break unless rules_state - end - - if rules_state - rule_set.actions.enabled.each do |action| - klass = action.class_name.constantize - email = klass.new.process(email) - # rescue NameError => ex - # # TODO: log a warning - end - end - - email - end - - def prepare_subjects(email, rule) - case rule.subject_type.downcase - when "header" - Array(email.header_values(rule.subject_value)) - when "body" - Array(email.plain_body) - else - raise ArgumentError, "Unrecognized subject type '#{rule.subject_type}'" - end - end - - def apply_rule(subjects, rule) - case rule.condition_type.downcase - when "match", "matches" - subjects.any? { |subject| - pattern = Regexp.new(rule.condition_value) - subject.match? - } - when "equal", "equals" - subjects.any? { |subject| - subject == rule.condition_value - } - when "start", "starts" - subjects.any? { |subject| - subject.starts_with? rule.condition_value - } - when "end", "ends" - subjects.any? { |subject| - subject.ends_with? rule.condition_value - } - when "contain", "contains" - subjects.any? { |subject| - subject.include? rule.condition_value - } - when "exist", "exists" - subjects.any? { |subject| - subject.exists? - } - when "empty" - subjects.all? { |subject| - subject.empty? - } - when "date_before" - # subjects.all? { |subject| - # subject.empty? - # } - when "date_after" - # subjects.all? { |subject| - # subject.empty? - # } - else - raise ArgumentError, "Unrecognized condition type '#{rule. condition_type}'" - end - - def apply_operator(state, operator, result) - case operator.upcase - when "AND" - (state and result) - when "OR" - (state or result) - when "XOR" - (state or result) and !(rules_state and result) - else - raise ArgumentError, "Unrecognized operator '#{operator}'" - end - end - - end - -end diff --git a/test/fixtures/rule_sets.yml b/test/fixtures/rule_sets.yml index f113155..df59850 100644 --- a/test/fixtures/rule_sets.yml +++ b/test/fixtures/rule_sets.yml @@ -23,19 +23,19 @@ or_rules: invalid_subject: name: Rules with an invalid rule subject - enabled: false + enabled: true operator: "AND" inverted: false invalid_condition_type: name: Rules with an invalid rule condition type - enabled: false + enabled: true operator: "AND" inverted: false invalid_operator: name: Rules with an invalid rule operator - enabled: false + enabled: true operator: "FOO" inverted: false diff --git a/test/services/rules_evaluator_test.rb b/test/services/rule_set_processor_test.rb similarity index 65% rename from test/services/rules_evaluator_test.rb rename to test/services/rule_set_processor_test.rb index 8cad1f8..97ff1d8 100644 --- a/test/services/rules_evaluator_test.rb +++ b/test/services/rule_set_processor_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -class RulesEvaluatorTest < ActiveSupport::TestCase +class RuleSetProcessorTest < ActiveSupport::TestCase test "mark cron from subject" do email = email_from_eml_with_rules("cron_subject.eml") @@ -57,33 +57,27 @@ class RulesEvaluatorTest < ActiveSupport::TestCase end test "invalid subject type" do - rules_evaluator = RulesEvaluator.new email = Email.new + processor = RuleSetProcessor.new + email = processor.process(rule_sets(:invalid_subject), email) - exception = assert_raise ArgumentError do - rules_evaluator.evaluate(email, rule_sets(:invalid_subject)) - end - assert_match(/^Unrecognized subject type/, exception.message) + assert_not_predicate email, :changed? end test "invalid condition type" do - rules_evaluator = RulesEvaluator.new email = Email.new + processor = RuleSetProcessor.new + email = processor.process(rule_sets(:invalid_condition_type), email) - exception = assert_raise ArgumentError do - rules_evaluator.evaluate(email, rule_sets(:invalid_condition_type)) - end - assert_match(/^Unrecognized condition type/, exception.message) + assert_not_predicate email, :changed? end test "invalid operator" do - rules_evaluator = RulesEvaluator.new email = Email.new + processor = RuleSetProcessor.new + email = processor.process(rule_sets(:invalid_operator), email) - exception = assert_raise ArgumentError do - rules_evaluator.evaluate(email, rule_sets(:invalid_operator)) - end - assert_match(/^Unrecognized operator/, exception.message) + assert_not_predicate email, :changed? end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5a19009..a7c5dbc 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,7 +4,7 @@ require 'rails/test_help' class ActiveSupport::TestCase # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) + # parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all @@ -21,10 +21,8 @@ class ActiveSupport::TestCase end def email_from_eml_with_rules(file_fixture_name) email = email_from_eml(file_fixture_name) - rules_evaluator = RulesEvaluator.new - email = rules_evaluator.evaluate(email, RuleSet.enabled) - - email + processor = RuleSetProcessor.new + email = processor.process_all(RuleSet.enabled, email) end def assert_no_html(text)