email_domain_checker 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ # Using Checker Class
2
+
3
+ The `EmailDomainChecker::Checker` class provides a more object-oriented approach to email validation.
4
+
5
+ ## Basic Usage
6
+
7
+ ```ruby
8
+ require 'email_domain_checker'
9
+
10
+ # Basic validation
11
+ checker = EmailDomainChecker::Checker.new("user@example.com")
12
+ if checker.valid?
13
+ puts "Valid email with valid domain"
14
+ end
15
+ ```
16
+
17
+ ## Validation Methods
18
+
19
+ ### Format Validation Only
20
+
21
+ ```ruby
22
+ checker = EmailDomainChecker::Checker.new("user@example.com", validate_domain: false)
23
+ checker.format_valid? # => true
24
+ ```
25
+
26
+ ### Domain Validation with MX Records
27
+
28
+ ```ruby
29
+ checker = EmailDomainChecker::Checker.new("user@example.com", check_mx: true)
30
+ checker.domain_valid? # => true if MX records exist
31
+ ```
32
+
33
+ ## Email Transformations
34
+
35
+ ### Normalized Email
36
+
37
+ Get the normalized (lowercase) version of the email:
38
+
39
+ ```ruby
40
+ checker = EmailDomainChecker::Checker.new("User@Example.COM")
41
+ checker.normalized_email # => "user@example.com"
42
+ ```
43
+
44
+ ### Canonical Email
45
+
46
+ Get the canonical version of the email (handles Gmail-style aliases):
47
+
48
+ ```ruby
49
+ checker = EmailDomainChecker::Checker.new("user.name+tag@gmail.com")
50
+ checker.canonical_email # => "username@gmail.com"
51
+ ```
52
+
53
+ ### Redacted Email
54
+
55
+ Get a redacted version of the email for privacy (shows hash instead of local part):
56
+
57
+ ```ruby
58
+ checker = EmailDomainChecker::Checker.new("user@example.com")
59
+ checker.redacted_email # => "{hash}@example.com"
60
+ ```
@@ -0,0 +1,95 @@
1
+ # Module-level Convenience Methods
2
+
3
+ The `EmailDomainChecker` module provides convenient class methods for quick validation and configuration.
4
+
5
+ ## Validation Methods
6
+
7
+ ### `valid?`
8
+
9
+ Quick validation with optional domain checking:
10
+
11
+ ```ruby
12
+ EmailDomainChecker.valid?("user@example.com", validate_domain: false) # => true
13
+ EmailDomainChecker.valid?("user@example.com", check_mx: true) # => true/false
14
+ ```
15
+
16
+ ### `format_valid?`
17
+
18
+ Validate email format only (skips domain validation):
19
+
20
+ ```ruby
21
+ EmailDomainChecker.format_valid?("user@example.com") # => true
22
+ EmailDomainChecker.format_valid?("invalid-email") # => false
23
+ ```
24
+
25
+ ### `domain_valid?`
26
+
27
+ Validate domain only (skips format validation):
28
+
29
+ ```ruby
30
+ EmailDomainChecker.domain_valid?("user@example.com", check_mx: true) # => true/false
31
+ EmailDomainChecker.domain_valid?("user@nonexistent.com", check_mx: true) # => false
32
+ ```
33
+
34
+ ## Utility Methods
35
+
36
+ ### `normalize`
37
+
38
+ Normalize email address to lowercase:
39
+
40
+ ```ruby
41
+ EmailDomainChecker.normalize("User@Example.COM") # => "user@example.com"
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ ### Global Configuration
47
+
48
+ ```ruby
49
+ EmailDomainChecker.configure(timeout: 10, check_mx: true)
50
+ ```
51
+
52
+ ### Block Configuration
53
+
54
+ ```ruby
55
+ EmailDomainChecker.configure do |config|
56
+ config.test_mode = true
57
+ config.cache_ttl = 3600
58
+ config.cache_enabled = true
59
+ end
60
+ ```
61
+
62
+ ## Cache Management
63
+
64
+ ### Clear All Cache
65
+
66
+ ```ruby
67
+ EmailDomainChecker.clear_cache
68
+ ```
69
+
70
+ ### Clear Cache for Specific Domain
71
+
72
+ ```ruby
73
+ EmailDomainChecker.clear_cache_for_domain("example.com")
74
+ ```
75
+
76
+ ### Using Cache with Blocks
77
+
78
+ ```ruby
79
+ # Method 1: Direct access via EmailDomainChecker.cache (recommended)
80
+ result = EmailDomainChecker.cache.with("my_key", ttl: 3600) do
81
+ # This block executes only when cache misses
82
+ expensive_computation
83
+ end
84
+
85
+ # Method 2: Using convenience method
86
+ result = EmailDomainChecker.with_cache("my_key", ttl: 3600) do
87
+ expensive_computation
88
+ end
89
+
90
+ # Force cache refresh
91
+ result = EmailDomainChecker.with_cache("my_key", force: true) do
92
+ # This block always executes
93
+ updated_computation
94
+ end
95
+ ```
@@ -0,0 +1,71 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>EmailDomainChecker Documentation</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 2rem;
13
+ background: #f5f5f5;
14
+ }
15
+ .container {
16
+ background: white;
17
+ border-radius: 8px;
18
+ padding: 2rem;
19
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
20
+ }
21
+ h1 {
22
+ color: #333;
23
+ margin-bottom: 0.5rem;
24
+ }
25
+ .subtitle {
26
+ color: #666;
27
+ margin-bottom: 2rem;
28
+ }
29
+ .versions {
30
+ list-style: none;
31
+ padding: 0;
32
+ }
33
+ .versions li {
34
+ margin: 0.5rem 0;
35
+ }
36
+ .versions a {
37
+ display: inline-block;
38
+ padding: 0.75rem 1.5rem;
39
+ background: #6200ea;
40
+ color: white;
41
+ text-decoration: none;
42
+ border-radius: 4px;
43
+ transition: background 0.2s;
44
+ }
45
+ .versions a:hover {
46
+ background: #7c3aed;
47
+ }
48
+ .version-label {
49
+ font-weight: 600;
50
+ margin-right: 0.5rem;
51
+ }
52
+ .latest-badge {
53
+ background: #4caf50;
54
+ color: white;
55
+ padding: 0.2rem 0.5rem;
56
+ border-radius: 3px;
57
+ font-size: 0.8rem;
58
+ margin-left: 0.5rem;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div class="container">
64
+ <h1>EmailDomainChecker Documentation</h1>
65
+ <p class="subtitle">Select a version to view documentation</p>
66
+ <ul class="versions">
67
+ {{VERSION_LIST}}
68
+ </ul>
69
+ </div>
70
+ </body>
71
+ </html>
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDomainChecker
4
+ module Cache
5
+ # Base class for cache adapters
6
+ # All cache adapters must implement these methods
7
+ class BaseAdapter
8
+ # Get cached value for a key
9
+ # @param key [String] cache key
10
+ # @return [Object, nil] cached value or nil if not found
11
+ def get(key)
12
+ raise NotImplementedError, "Subclasses must implement #get"
13
+ end
14
+
15
+ # Set a value in cache
16
+ # @param key [String] cache key
17
+ # @param value [Object] value to cache
18
+ # @param ttl [Integer, nil] time to live in seconds (nil for no expiration)
19
+ # @return [void]
20
+ def set(key, value, ttl: nil)
21
+ raise NotImplementedError, "Subclasses must implement #set"
22
+ end
23
+
24
+ # Delete a key from cache
25
+ # @param key [String] cache key
26
+ # @return [void]
27
+ def delete(key)
28
+ raise NotImplementedError, "Subclasses must implement #delete"
29
+ end
30
+
31
+ # Clear all cache entries
32
+ # @return [void]
33
+ def clear
34
+ raise NotImplementedError, "Subclasses must implement #clear"
35
+ end
36
+
37
+ # Check if a key exists in cache
38
+ # @param key [String] cache key
39
+ # @return [Boolean] true if key exists
40
+ def exists?(key)
41
+ get(key) != nil
42
+ end
43
+
44
+ # Fetch value from cache or execute block and cache the result
45
+ # Similar to Rails.cache.fetch
46
+ # @param key [String] cache key
47
+ # @param ttl [Integer, nil] time to live in seconds (nil for no expiration)
48
+ # @param force [Boolean] if true, always execute block and update cache
49
+ # @yield Block to execute when cache miss
50
+ # @return [Object] cached value or block result
51
+ def with(key, ttl: nil, force: false, &block)
52
+ raise ArgumentError, "Block is required" unless block_given?
53
+
54
+ # Return cached value if not forcing and cache exists
55
+ unless force
56
+ return get(key) if exists?(key)
57
+ end
58
+
59
+ # Execute block and cache the result
60
+ # Rails.cache.fetch also caches nil values, so we do the same
61
+ value = yield
62
+ set(key, value, ttl: ttl)
63
+ value
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_adapter"
4
+
5
+ module EmailDomainChecker
6
+ module Cache
7
+ # In-memory cache adapter using a simple hash
8
+ # This is the default cache adapter when Redis is not available
9
+ class MemoryAdapter < BaseAdapter
10
+ def initialize
11
+ @store = {}
12
+ @expirations = {}
13
+ @nil_keys = {} # Track keys that have nil values
14
+ end
15
+
16
+ def get(key)
17
+ # Check if key exists (including nil values)
18
+ return nil unless @store.key?(key) || @nil_keys.key?(key)
19
+
20
+ # Check if expired
21
+ if @expirations.key?(key) && Time.now > @expirations[key]
22
+ delete(key)
23
+ return nil
24
+ end
25
+
26
+ # Return nil if it was explicitly cached, otherwise return stored value
27
+ return nil if @nil_keys.key?(key)
28
+
29
+ @store[key]
30
+ end
31
+
32
+ def set(key, value, ttl: nil)
33
+ if value.nil?
34
+ # Store nil separately to distinguish from "not cached"
35
+ @nil_keys[key] = true
36
+ @store.delete(key)
37
+ else
38
+ @store[key] = value
39
+ @nil_keys.delete(key)
40
+ end
41
+
42
+ if ttl
43
+ @expirations[key] = Time.now + ttl
44
+ else
45
+ @expirations.delete(key)
46
+ end
47
+ value
48
+ end
49
+
50
+ def delete(key)
51
+ @store.delete(key)
52
+ @nil_keys.delete(key)
53
+ @expirations.delete(key)
54
+ end
55
+
56
+ def clear
57
+ @store.clear
58
+ @nil_keys.clear
59
+ @expirations.clear
60
+ end
61
+
62
+ def exists?(key)
63
+ return false unless @store.key?(key) || @nil_keys.key?(key)
64
+
65
+ # Check if expired
66
+ if @expirations.key?(key) && Time.now > @expirations[key]
67
+ delete(key)
68
+ return false
69
+ end
70
+
71
+ true
72
+ end
73
+
74
+ # Get cache size (for debugging/monitoring)
75
+ def size
76
+ # Clean expired entries first
77
+ clean_expired
78
+ @store.size + @nil_keys.size
79
+ end
80
+
81
+ private
82
+
83
+ def clean_expired
84
+ now = Time.now
85
+ @expirations.each do |key, expiration|
86
+ delete(key) if now > expiration
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_adapter"
4
+
5
+ module EmailDomainChecker
6
+ module Cache
7
+ # Redis cache adapter
8
+ # Requires the 'redis' gem to be available
9
+ class RedisAdapter < BaseAdapter
10
+ def initialize(redis_client = nil)
11
+ @redis = redis_client || create_default_redis_client
12
+ end
13
+
14
+ def get(key)
15
+ value = @redis.get(key)
16
+ return nil unless value
17
+
18
+ # Parse JSON value
19
+ JSON.parse(value)
20
+ rescue JSON::ParserError, Redis::BaseError
21
+ nil
22
+ end
23
+
24
+ def set(key, value, ttl: nil)
25
+ json_value = JSON.generate(value)
26
+ if ttl
27
+ @redis.setex(key, ttl, json_value)
28
+ else
29
+ @redis.set(key, json_value)
30
+ end
31
+ value
32
+ rescue Redis::BaseError
33
+ # Silently fail if Redis is unavailable
34
+ value
35
+ end
36
+
37
+ def delete(key)
38
+ @redis.del(key)
39
+ rescue Redis::BaseError
40
+ # Silently fail if Redis is unavailable
41
+ end
42
+
43
+ def clear
44
+ @redis.flushdb
45
+ rescue Redis::BaseError
46
+ # Silently fail if Redis is unavailable
47
+ end
48
+
49
+ def exists?(key)
50
+ @redis.exists?(key)
51
+ rescue Redis::BaseError
52
+ false
53
+ end
54
+
55
+ private
56
+
57
+ def create_default_redis_client
58
+ require "redis"
59
+ require "json"
60
+ Redis.new
61
+ rescue LoadError
62
+ raise LoadError, "Redis gem is required for RedisAdapter. Please add 'gem \"redis\"' to your Gemfile."
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cache/base_adapter"
4
+ require_relative "cache/memory_adapter"
5
+
6
+ # Conditionally load Redis adapter if available
7
+ begin
8
+ require "redis"
9
+ require "json"
10
+ require_relative "cache/redis_adapter"
11
+ rescue LoadError
12
+ # Redis is not available, skip Redis adapter
13
+ end
14
+
15
+ module EmailDomainChecker
16
+ module Cache
17
+ # Factory method to create cache adapter based on configuration
18
+ #
19
+ # @param type [Symbol, Class, Object] Cache adapter type, class, or instance
20
+ # - Symbol: :memory or :redis
21
+ # - Class: A custom adapter class (must inherit from BaseAdapter)
22
+ # - Object: A custom adapter instance (must be an instance of BaseAdapter)
23
+ # @param redis_client [Redis, nil] Redis client instance (only used for :redis type)
24
+ # @return [BaseAdapter] Cache adapter instance
25
+ def self.create_adapter(type: :memory, redis_client: nil)
26
+ # If type is already an adapter instance, return it
27
+ return type if type.is_a?(BaseAdapter)
28
+
29
+ # If type is a Class, instantiate it
30
+ if type.is_a?(Class)
31
+ unless type < BaseAdapter
32
+ raise ArgumentError, "Custom cache adapter class must inherit from EmailDomainChecker::Cache::BaseAdapter"
33
+ end
34
+ return type.new
35
+ end
36
+
37
+ # Handle symbol types
38
+ case type.to_sym
39
+ when :memory
40
+ MemoryAdapter.new
41
+ when :redis
42
+ if defined?(RedisAdapter)
43
+ RedisAdapter.new(redis_client)
44
+ else
45
+ # Fallback to memory if Redis is not available
46
+ warn "Redis adapter requested but 'redis' gem is not available. Falling back to memory cache."
47
+ MemoryAdapter.new
48
+ end
49
+ else
50
+ raise ArgumentError, "Unknown cache adapter type: #{type}. Available: :memory, :redis, or a custom BaseAdapter class/instance"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "cache"
4
+
3
5
  module EmailDomainChecker
4
6
  class Config
5
7
  DEFAULT_OPTIONS = {
@@ -11,7 +13,7 @@ module EmailDomainChecker
11
13
  }.freeze
12
14
 
13
15
  class << self
14
- attr_accessor :default_options, :test_mode
16
+ attr_accessor :default_options, :test_mode, :cache_enabled, :cache_type, :cache_ttl, :cache_adapter, :cache_adapter_instance, :redis_client
15
17
 
16
18
  def configure(options = {}, &block)
17
19
  if block_given?
@@ -28,6 +30,12 @@ module EmailDomainChecker
28
30
  def reset
29
31
  @default_options = DEFAULT_OPTIONS.dup
30
32
  @test_mode = false
33
+ @cache_enabled = true
34
+ @cache_type = :memory
35
+ @cache_ttl = 3600
36
+ @cache_adapter = nil
37
+ @cache_adapter_instance = nil
38
+ @redis_client = nil
31
39
  end
32
40
 
33
41
  def test_mode=(value)
@@ -37,12 +45,86 @@ module EmailDomainChecker
37
45
  def test_mode?
38
46
  @test_mode == true
39
47
  end
48
+
49
+ def cache_enabled?
50
+ @cache_enabled == true
51
+ end
52
+
53
+ def cache_adapter
54
+ return nil unless cache_enabled?
55
+
56
+ # If custom adapter instance is set, use it directly
57
+ return @cache_adapter_instance if @cache_adapter_instance
58
+
59
+ # Otherwise, create adapter from type
60
+ @cache_adapter ||= Cache.create_adapter(type: cache_type, redis_client: redis_client)
61
+ end
62
+
63
+ def cache_adapter_instance=(instance)
64
+ validate_cache_adapter_instance!(instance)
65
+ @cache_adapter_instance = instance
66
+ # Reset the auto-created adapter when custom instance is set
67
+ @cache_adapter = nil
68
+ end
69
+
70
+ def cache_type=(value)
71
+ @cache_type = value
72
+ reset_cache_adapter_if_needed
73
+ end
74
+
75
+ def clear_cache
76
+ cache_adapter&.clear
77
+ end
78
+
79
+ def clear_cache_for_domain(domain)
80
+ return unless cache_enabled?
81
+
82
+ adapter = cache_adapter
83
+ return unless adapter
84
+
85
+ # Clear both MX and A record cache entries
86
+ dns_cache_keys_for_domain(domain).each do |key|
87
+ adapter.delete(key)
88
+ end
89
+ end
90
+
91
+ def reset_cache_adapter_if_needed
92
+ # Reset cache adapter when changing cache type (unless custom instance is set)
93
+ @cache_adapter = nil unless @cache_adapter_instance
94
+ # Clear cache_adapter_instance if setting a type (Symbol or String), not a Class
95
+ @cache_adapter_instance = nil if @cache_type.is_a?(Symbol) || @cache_type.is_a?(String)
96
+ end
97
+
98
+ def reset_cache_adapter_on_enabled_change(new_value, old_value)
99
+ @cache_adapter = nil if new_value != old_value
100
+ end
101
+
102
+ def reset_cache_adapter_if_redis
103
+ @cache_adapter = nil if @cache_type == :redis
104
+ end
105
+
106
+ def validate_cache_adapter_instance!(instance)
107
+ unless instance.nil? || instance.is_a?(Cache::BaseAdapter)
108
+ raise ArgumentError, "cache_adapter_instance must be an instance of EmailDomainChecker::Cache::BaseAdapter or nil"
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def dns_cache_keys_for_domain(domain)
115
+ ["mx:#{domain}", "a:#{domain}"]
116
+ end
40
117
  end
41
118
 
42
- attr_accessor :test_mode
119
+ attr_accessor :test_mode, :cache_enabled, :cache_type, :cache_ttl, :cache_adapter_instance, :redis_client
43
120
 
44
121
  def initialize
45
122
  @test_mode = self.class.test_mode || false
123
+ @cache_enabled = self.class.cache_enabled.nil? ? true : self.class.cache_enabled
124
+ @cache_type = self.class.cache_type || :memory
125
+ @cache_ttl = self.class.cache_ttl || 3600
126
+ @cache_adapter_instance = self.class.cache_adapter_instance
127
+ @redis_client = self.class.redis_client
46
128
  end
47
129
 
48
130
  def test_mode=(value)
@@ -50,6 +132,38 @@ module EmailDomainChecker
50
132
  self.class.test_mode = value
51
133
  end
52
134
 
135
+ def cache_enabled=(value)
136
+ old_value = self.class.cache_enabled
137
+ @cache_enabled = value
138
+ self.class.cache_enabled = value
139
+ # Reset cache adapter when enabling/disabling cache
140
+ self.class.reset_cache_adapter_on_enabled_change(value, old_value)
141
+ end
142
+
143
+ def cache_type=(value)
144
+ @cache_type = value
145
+ self.class.cache_type = value
146
+ self.class.reset_cache_adapter_if_needed
147
+ end
148
+
149
+ def cache_adapter_instance=(instance)
150
+ self.class.validate_cache_adapter_instance!(instance)
151
+ @cache_adapter_instance = instance
152
+ self.class.cache_adapter_instance = instance
153
+ end
154
+
155
+ def cache_ttl=(value)
156
+ @cache_ttl = value
157
+ self.class.cache_ttl = value
158
+ end
159
+
160
+ def redis_client=(value)
161
+ @redis_client = value
162
+ self.class.redis_client = value
163
+ # Reset cache adapter when changing redis client
164
+ self.class.reset_cache_adapter_if_redis
165
+ end
166
+
53
167
  reset
54
168
  end
55
169
  end