From 900d5a4d1b2334cdce61bd51707c788036381d58 Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 10 Dec 2025 19:47:07 +0100 Subject: [PATCH 1/2] add rules to the gem --- README.md | 85 +++++++++++++++++++- lib/improvmx/client.rb | 2 + lib/improvmx/rules.rb | 149 ++++++++++++++++++++++++++++++++++++ spec/improvmx/rules_spec.rb | 147 +++++++++++++++++++++++++++++++++++ 4 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 lib/improvmx/rules.rb create mode 100644 spec/improvmx/rules_spec.rb diff --git a/README.md b/README.md index 7f02426..4b9a445 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ImprovMX Ruby Gem Ruby interface to connect to the ImprovMX API. -Currently still work in progress, it only contains the aliases endpoints at the moment. +Supports both aliases and rules endpoints. Installation @@ -17,16 +17,95 @@ Usage ----- This is how you can use this gem +### Aliases + ```ruby require 'improvmx' -# Instantiate the Client with your API key and domain +# Instantiate the Client with your API key client = Improvmx::Client.new 'your-api-key' # List all the aliases aliases = client.list_aliases('domain.com') - puts aliases['aliases'] + +# Create an alias +client.create_alias('hello', 'receiver@example.com', 'domain.com') + +# Get a specific alias +alias_info = client.get_alias('hello', 'domain.com') + +# Update an alias +client.update_alias('hello', 'new_receiver@example.com', 'domain.com') + +# Delete an alias +client.delete_alias('hello', 'domain.com') +``` + +### Rules + +Rules provide advanced email routing based on patterns, regular expressions, or conditions. + +```ruby +require 'improvmx' + +client = Improvmx::Client.new 'your-api-key' + +# List all rules +rules = client.list_rules('domain.com') +puts rules['rules'] + +# Create an alias rule (matches specific alias) +rule = client.create_alias_rule('domain.com', 'support', 'support@company.com') +puts rule['id'] # => UUID of the created rule + +# Create a regex rule (matches based on regex pattern) +rule = client.create_regex_rule( + 'domain.com', + '.*important.*', # Regex pattern + ['subject', 'body'], # Scopes to match + 'urgent@company.com' # Forward destination +) + +# Create a CEL rule (matches based on CEL expression) +rule = client.create_cel_rule( + 'domain.com', + "subject.contains('invoice')", # CEL expression + 'billing@company.com' +) + +# Create a rule with custom rank and active state +rule = client.create_alias_rule( + 'domain.com', + 'priority', + 'priority@company.com', + rank: 1.0, # Lower rank = higher priority + active: true +) + +# Get a specific rule by ID +rule = client.get_rule('rule-uuid', 'domain.com') + +# Update a rule's config +client.update_rule( + 'rule-uuid', + 'domain.com', + config: { alias: 'support', forward: 'newsupport@company.com' }, + active: false +) + +# Delete a rule +client.delete_rule('rule-uuid', 'domain.com') + +# Delete all rules +client.delete_all_rules('domain.com') + +# Bulk add multiple rules +rules = [ + { type: 'alias', config: { alias: 'info', forward: 'info@company.com' } }, + { type: 'alias', config: { alias: 'sales', forward: 'sales@company.com' } } +] +client.bulk_modify_rules('domain.com', rules, behavior: 'add') ``` Improvmx has a rate limit system, to handle this you can do diff --git a/lib/improvmx/client.rb b/lib/improvmx/client.rb index 33dfada..4221912 100755 --- a/lib/improvmx/client.rb +++ b/lib/improvmx/client.rb @@ -1,4 +1,5 @@ require 'improvmx/aliases' +require 'improvmx/rules' require 'improvmx/smtp' require 'improvmx/response' require 'improvmx/utils' @@ -7,6 +8,7 @@ module Improvmx class Client include Improvmx::Aliases + include Improvmx::Rules include Improvmx::SMTP include Improvmx::Utils diff --git a/lib/improvmx/rules.rb b/lib/improvmx/rules.rb new file mode 100644 index 0000000..b83699b --- /dev/null +++ b/lib/improvmx/rules.rb @@ -0,0 +1,149 @@ +module Improvmx + # All rule related endpoints + module Rules + # List all rules for a domain + # @param domain [String] The domain name + # @param params [Hash] Optional parameters (search, page) + # @return [Hash] Response containing rules array + def list_rules(domain, params = {}) + get("/domains/#{domain}/rules/", params).to_h + end + + # Get a specific rule + # @param rule_id [String] The rule ID (UUID) + # @param domain [String] The domain name + # @return [Hash, nil] The rule object or nil if not found + def get_rule(rule_id, domain) + get("/domains/#{domain}/rules/#{rule_id}").to_h + rescue NotFoundError + nil + end + + # Create an alias rule + # @param domain [String] The domain name + # @param alias_name [String] The alias (e.g., "richard") + # @param forward_to [String, Array] Destination email(s) + # @param rank [Float, nil] Optional rank for evaluation priority + # @param active [Boolean] Whether the rule is active (default: true) + # @param id [String, nil] Optional custom rule ID + # @return [Hash] The created rule + def create_alias_rule(domain, alias_name, forward_to, rank: nil, active: true, id: nil) + data = { + type: 'alias', + config: { + alias: alias_name, + forward: forward(forward_to) + }, + active: active + } + data[:rank] = rank if rank + data[:id] = id if id + + response = post("/domains/#{domain}/rules/", data) + response.to_h['rule'] + end + + # Create a regex rule + # @param domain [String] The domain name + # @param regex [String] The regular expression pattern + # @param scopes [Array] Scopes to match (sender, recipient, subject, body) + # @param forward_to [String, Array] Destination email(s) + # @param rank [Float, nil] Optional rank for evaluation priority + # @param active [Boolean] Whether the rule is active (default: true) + # @param id [String, nil] Optional custom rule ID + # @return [Hash] The created rule + def create_regex_rule(domain, regex, scopes, forward_to, rank: nil, active: true, id: nil) + data = { + type: 'regex', + config: { + regex: regex, + scopes: scopes, + forward: forward(forward_to) + }, + active: active + } + data[:rank] = rank if rank + data[:id] = id if id + + response = post("/domains/#{domain}/rules/", data) + response.to_h['rule'] + end + + # Create a CEL rule + # @param domain [String] The domain name + # @param expression [String] The CEL expression + # @param forward_to [String, Array] Destination email(s) + # @param rank [Float, nil] Optional rank for evaluation priority + # @param active [Boolean] Whether the rule is active (default: true) + # @param id [String, nil] Optional custom rule ID + # @return [Hash] The created rule + def create_cel_rule(domain, expression, forward_to, rank: nil, active: true, id: nil) + data = { + type: 'cel', + config: { + expression: expression, + forward: forward(forward_to) + }, + active: active + } + data[:rank] = rank if rank + data[:id] = id if id + + response = post("/domains/#{domain}/rules/", data) + response.to_h['rule'] + end + + # Update a rule + # @param rule_id [String] The rule ID + # @param domain [String] The domain name + # @param config [Hash] The config object for the rule + # @param rank [Float, nil] Optional new rank + # @param active [Boolean, nil] Optional active state + # @return [Hash, false] The updated rule or false if not found + def update_rule(rule_id, domain, config: nil, rank: nil, active: nil) + data = {} + data[:config] = config if config + data[:rank] = rank if rank + data[:active] = active unless active.nil? + + response = put("/domains/#{domain}/rules/#{rule_id}", data) + response.to_h['rule'] + rescue NotFoundError + false + end + + # Delete a rule + # @param rule_id [String] The rule ID + # @param domain [String] The domain name + # @return [Boolean] true if deleted or not found + def delete_rule(rule_id, domain) + response = delete("/domains/#{domain}/rules/#{rule_id}") + response.ok? + rescue NotFoundError + true + end + + # Delete all rules for a domain + # @param domain [String] The domain name + # @return [Boolean] true if successful + def delete_all_rules(domain) + response = delete("/domains/#{domain}/rules-all") + response.ok? + end + + # Bulk modify rules + # @param domain [String] The domain name + # @param rules [Array] Array of rule objects + # @param behavior [String] 'add', 'update', or 'delete' (default: 'add') + # @return [Hash] Results of bulk operation + def bulk_modify_rules(domain, rules, behavior: 'add') + data = { + rules: rules, + behavior: behavior + } + + response = post("/domains/#{domain}/rules/bulk", data) + response.to_h + end + end +end diff --git a/spec/improvmx/rules_spec.rb b/spec/improvmx/rules_spec.rb new file mode 100644 index 0000000..84d70c2 --- /dev/null +++ b/spec/improvmx/rules_spec.rb @@ -0,0 +1,147 @@ +require 'spec_helper' +require 'improvmx' + +describe Improvmx::Rules do + let(:client) { Improvmx::Client.new(APIKEY) } + let(:forward_to) { 'receiver@example.com' } + let(:other_forward_to) { 'new_receiver@example.com' } + let(:alias_name) { 'testrule' } + + describe '#create_alias_rule' do + it 'creates an alias rule' do + response = client.create_alias_rule(DOMAIN, alias_name, forward_to) + + expect(response).to be_a(Hash) + expect(response['type']).to eq 'alias' + expect(response['config']['alias']).to eq alias_name + expect(response['id']).not_to be_nil + end + end + + describe '#create_regex_rule' do + it 'creates a regex rule' do + response = client.create_regex_rule(DOMAIN, '.*test.*', ['subject'], forward_to) + + expect(response).to be_a(Hash) + expect(response['type']).to eq 'regex' + expect(response['config']['regex']).to eq '.*test.*' + expect(response['id']).not_to be_nil + end + end + + describe '#create_cel_rule' do + it 'creates a CEL rule' do + response = client.create_cel_rule(DOMAIN, 'true', forward_to) + + expect(response).to be_a(Hash) + expect(response['type']).to eq 'cel' + expect(response['config']['expression']).to eq 'true' + expect(response['id']).not_to be_nil + end + end + + describe '#list_rules' do + it 'shows all rules' do + response = client.list_rules(DOMAIN) + + expect(response['rules']).to be_an(Array) + expect(response).to have_key('success') + end + end + + describe '#get_rule' do + before do + rules = client.list_rules(DOMAIN) + @rule_id = rules['rules'].first['id'] if rules['rules'].any? + end + + it 'shows a specific rule' do + skip 'No rules available' unless @rule_id + + response = client.get_rule(@rule_id, DOMAIN) + + expect(response['id']).to eq @rule_id + expect(response['type']).not_to be_nil + end + + it 'gives nil for invalid rule' do + response = client.get_rule('non-existing-id', DOMAIN) + + expect(response).to eq nil + end + end + + describe '#update_rule' do + before do + rules = client.list_rules(DOMAIN) + @rule_id = rules['rules'].first['id'] if rules['rules'].any? + @rule_type = rules['rules'].first['type'] if rules['rules'].any? + end + + it 'updates rule' do + skip 'No rules available' unless @rule_id + + new_config = if @rule_type == 'alias' + { alias: alias_name, forward: other_forward_to } + elsif @rule_type == 'regex' + { regex: '.*updated.*', scopes: ['subject'], forward: other_forward_to } + else + { expression: 'true', forward: other_forward_to } + end + + response = client.update_rule(@rule_id, DOMAIN, config: new_config) + + expect(response).to be_a(Hash) + expect(response['config']['forward']).to eq other_forward_to + end + + it 'returns false on non-existing rule' do + response = client.update_rule('wrong-id', DOMAIN, config: { forward: other_forward_to }) + + expect(response).to eq false + end + end + + describe '#delete_rule' do + before do + rules = client.list_rules(DOMAIN) + @rule_id = rules['rules'].first['id'] if rules['rules'].any? + end + + it 'deletes a rule' do + skip 'No rules available' unless @rule_id + + response = client.delete_rule(@rule_id, DOMAIN) + + expect(response).to be true + end + + it 'returns true for non-existing rule' do + response = client.delete_rule('wrong-id', DOMAIN) + + expect(response).to be true + end + end + + describe '#delete_all_rules' do + it 'deletes all rules' do + response = client.delete_all_rules(DOMAIN) + + expect(response).to be true + end + end + + describe '#bulk_modify_rules' do + it 'adds multiple rules' do + rules = [ + { type: 'alias', config: { alias: 'bulk1', forward: forward_to } }, + { type: 'alias', config: { alias: 'bulk2', forward: forward_to } } + ] + + response = client.bulk_modify_rules(DOMAIN, rules, behavior: 'add') + + expect(response['success']).to be true + expect(response['results']).to be_an(Array) + end + end +end From 343daebf9e3193c49806ef1c2d3e3131daf5bdc0 Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 10 Dec 2025 20:06:48 +0100 Subject: [PATCH 2/2] update depencies --- Gemfile | 22 +++++++++++----------- improvmx.gemspec | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index ddaef5d..5168b65 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,17 @@ source 'https://rubygems.org' group :development do - gem 'bundler', '>= 2.2' - gem'dotenv', '~> 2.7.0' - gem'pry', '~> 0.14.0' - gem'rails' - gem'rake', '~> 13.0.0' - gem'rspec', '~> 3.10.0' - gem'rubocop', '~> 1.11.0' - gem'rubocop-rspec', '~> 2.2.0' - gem'simplecov', '~> 0.21.0' - gem'vcr', '~> 6.0.0' - gem'webmock', '~> 3.12.0' + gem 'bundler', '>= 2.5' + gem 'dotenv', '~> 3.1' + gem 'pry', '~> 0.14.2' + gem 'rails' + gem 'rake', '~> 13.2' + gem 'rspec', '~> 3.13' + gem 'rubocop', '~> 1.69' + gem 'rubocop-rspec', '~> 3.2' + gem 'simplecov', '~> 0.22' + gem 'vcr', '~> 6.3' + gem 'webmock', '~> 3.24' end gemspec \ No newline at end of file diff --git a/improvmx.gemspec b/improvmx.gemspec index f61de96..af569d0 100644 --- a/improvmx.gemspec +++ b/improvmx.gemspec @@ -15,6 +15,6 @@ Gem::Specification.new do |spec| spec.files = %w[LICENSE README.md improvmx.gemspec] + Dir['lib/**/*.rb'] spec.require_paths = %w[lib] - spec.required_ruby_version = '>= 2.4' - spec.add_dependency 'rest-client', '~> 2.0' + spec.required_ruby_version = '>= 3.0' + spec.add_dependency 'rest-client', '~> 2.1' end