email_domain_checker 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 946d5e9368662216b21d1dfa1434edbdbe992b6c1354d3b53b73a62e8d262a06
4
+ data.tar.gz: 9e47f0583115b145f461d7fcbc2cda5a4db3abe263a8edab82df98d160242431
5
+ SHA512:
6
+ metadata.gz: c333b2f6dd62889860581493a2453ee0f16db2f9db024f3f3ff8db88c2496e4d438221ff31a55d8fc2fd3174d5aec834615e221a2e49b377c22dc4bc3c569ed3
7
+ data.tar.gz: 2fd79606ecdf91e1c665b71ccb343f2b3703082db2c5d63085919435fabc7955f89cae73d2a66a8fefb9c8d4d26611b25346e06c4b69f6e1958852f2228f8b0e
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+ Exclude:
5
+ - "bin/**/*"
6
+ - "tmp/**/*"
7
+ - "vendor/**/*"
8
+
9
+ Style/Documentation:
10
+ Enabled: false
11
+
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - "spec/**/*"
15
+ - "*.gemspec"
16
+
17
+ Style/FrozenStringLiteralComment:
18
+ Enabled: true
19
+
20
+ Layout/LineLength:
21
+ Max: 120
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-11-4
9
+
10
+ ### Added
11
+ - Initial release
12
+ - Email format validation using `email_address` gem
13
+ - Domain validation with MX and A record checks
14
+ - Email normalization and canonicalization
15
+ - Email redaction for privacy
16
+ - Module-level convenience methods
17
+ - Configurable default options
18
+ - DNS resolver with timeout support
19
+ - Comprehensive test coverage
20
+
21
+ ### Features
22
+ - `EmailDomainChecker::Checker` class for detailed validation
23
+ - `EmailDomainChecker.valid?` for quick validation
24
+ - `EmailDomainChecker.format_valid?` for format-only checks
25
+ - `EmailDomainChecker.domain_valid?` for domain-only checks
26
+ - `EmailDomainChecker.normalize` for email normalization
27
+ - `EmailDomainChecker.configure` for global configuration
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # EmailDomainChecker
2
+
3
+ Email address validation and domain checking library to prevent mail server reputation degradation.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'email_domain_checker'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install email_domain_checker
20
+
21
+ ## Usage
22
+
23
+ ### Module-level convenience methods
24
+
25
+ ```ruby
26
+ require 'email_domain_checker'
27
+
28
+ # Quick validation
29
+ EmailDomainChecker.valid?("user@example.com", validate_domain: false) # => true
30
+
31
+ # Format validation only
32
+ EmailDomainChecker.format_valid?("user@example.com") # => true
33
+
34
+ # Domain validation only
35
+ EmailDomainChecker.domain_valid?("user@example.com", check_mx: true) # => true/false
36
+
37
+ # Normalize email
38
+ EmailDomainChecker.normalize("User@Example.COM") # => "user@example.com"
39
+
40
+ # Configure default options globally
41
+ EmailDomainChecker.configure(timeout: 10, check_mx: true)
42
+ ```
43
+
44
+ ### Using Checker class
45
+
46
+ ```ruby
47
+ require 'email_domain_checker'
48
+
49
+ # Basic validation
50
+ checker = EmailDomainChecker::Checker.new("user@example.com")
51
+ if checker.valid?
52
+ puts "Valid email with valid domain"
53
+ end
54
+
55
+ # Check format only
56
+ checker = EmailDomainChecker::Checker.new("user@example.com", validate_domain: false)
57
+ checker.format_valid? # => true
58
+
59
+ # Check domain with MX records
60
+ checker = EmailDomainChecker::Checker.new("user@example.com", check_mx: true)
61
+ checker.domain_valid? # => true if MX records exist
62
+
63
+ # Get normalized email
64
+ checker = EmailDomainChecker::Checker.new("User@Example.COM")
65
+ checker.normalized_email # => "user@example.com"
66
+
67
+ # Get canonical email
68
+ checker = EmailDomainChecker::Checker.new("user.name+tag@gmail.com")
69
+ checker.canonical_email # => "username@gmail.com"
70
+
71
+ # Get redacted email (for privacy)
72
+ checker = EmailDomainChecker::Checker.new("user@example.com")
73
+ checker.redacted_email # => "{hash}@example.com"
74
+ ```
75
+
76
+ ### Configuration Options
77
+
78
+ - `validate_format`: Validate email format using email_address gem (default: true)
79
+ - `validate_domain`: Validate domain existence (default: true)
80
+ - `check_mx`: Check MX records for domain (default: true)
81
+ - `check_a`: Check A records for domain (default: false)
82
+ - `timeout`: DNS lookup timeout in seconds (default: 5)
83
+
84
+ ## Development
85
+
86
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
87
+
88
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
89
+
90
+ ## Contributing
91
+
92
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/email_domain_checker.
93
+
94
+ ## License
95
+
96
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/email_domain_checker/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "email_domain_checker"
7
+ spec.version = EmailDomainChecker::VERSION
8
+ spec.authors = ["Koki Tatematsu"]
9
+ spec.email = ["koki.tatematsu@gmail.com"]
10
+
11
+ spec.summary = "Email address validation and domain checking library"
12
+ spec.description = "A library to validate email addresses and check domain validity to prevent mail server reputation degradation"
13
+ spec.homepage = "https://github.com/tatematsu-k/email_domain_checker"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
21
+
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "email_address", "~> 0.2"
33
+ spec.add_development_dependency "rake", "~> 13.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "rubocop", "~> 1.0"
36
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "normalizer"
4
+ require_relative "domain_validator"
5
+ require_relative "email_address_adapter"
6
+
7
+ module EmailDomainChecker
8
+ class Checker
9
+ attr_reader :email, :options
10
+
11
+ def initialize(email, options = {})
12
+ @email = Normalizer.normalize(email)
13
+ @options = Config.default_options.merge(options)
14
+ @domain_validator = DomainValidator.new(
15
+ check_mx: @options[:check_mx],
16
+ check_a: @options[:check_a],
17
+ timeout: @options[:timeout]
18
+ )
19
+ end
20
+
21
+ def valid?
22
+ return false if email.empty?
23
+
24
+ format_valid? && domain_valid?
25
+ end
26
+
27
+ def format_valid?
28
+ return true unless options[:validate_format]
29
+
30
+ email_adapter.valid?
31
+ end
32
+
33
+ def domain_valid?
34
+ return true unless options[:validate_domain]
35
+
36
+ domain = extract_domain
37
+ @domain_validator.valid?(domain)
38
+ end
39
+
40
+ def normalized_email
41
+ return nil unless format_valid?
42
+
43
+ email_adapter.normalized
44
+ end
45
+
46
+ def canonical_email
47
+ return nil unless format_valid?
48
+
49
+ email_adapter.canonical
50
+ end
51
+
52
+ def redacted_email
53
+ return nil unless format_valid?
54
+
55
+ email_adapter.redacted
56
+ end
57
+
58
+ private
59
+
60
+ def email_adapter
61
+ @email_adapter ||= EmailAddressAdapter.new(email)
62
+ end
63
+
64
+ def extract_domain
65
+ parts = email.split("@", 2)
66
+ return nil if parts.length != 2
67
+
68
+ domain = parts[1].to_s.strip
69
+ domain.empty? ? nil : domain
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDomainChecker
4
+ class Config
5
+ DEFAULT_OPTIONS = {
6
+ validate_format: true,
7
+ validate_domain: true,
8
+ check_mx: true,
9
+ check_a: false,
10
+ timeout: 5
11
+ }.freeze
12
+
13
+ class << self
14
+ attr_accessor :default_options
15
+
16
+ def configure(options = {})
17
+ @default_options = DEFAULT_OPTIONS.merge(options)
18
+ end
19
+
20
+ def reset
21
+ @default_options = DEFAULT_OPTIONS.dup
22
+ end
23
+ end
24
+
25
+ reset
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+
5
+ module EmailDomainChecker
6
+ class DnsResolver
7
+ attr_reader :timeout
8
+
9
+ def initialize(timeout: 5)
10
+ @timeout = timeout
11
+ end
12
+
13
+ def has_mx_record?(domain)
14
+ check_dns_record(domain, Resolv::DNS::Resource::IN::MX)
15
+ end
16
+
17
+ def has_a_record?(domain)
18
+ check_dns_record(domain, Resolv::DNS::Resource::IN::A)
19
+ end
20
+
21
+ private
22
+
23
+ def check_dns_record(domain, resource_type)
24
+ resolver = create_resolver
25
+
26
+ begin
27
+ records = resolver.getresources(domain, resource_type)
28
+ !records.empty?
29
+ rescue Resolv::ResolvError, Resolv::ResolvTimeout
30
+ false
31
+ end
32
+ end
33
+
34
+ def create_resolver
35
+ resolver = Resolv::DNS.new
36
+ resolver.timeouts = [timeout]
37
+ resolver
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dns_resolver"
4
+
5
+ module EmailDomainChecker
6
+ class DomainValidator
7
+ attr_reader :options, :dns_resolver
8
+
9
+ def initialize(options = {})
10
+ @options = {
11
+ check_mx: true,
12
+ check_a: false,
13
+ timeout: 5
14
+ }.merge(options)
15
+ @dns_resolver = DnsResolver.new(timeout: @options[:timeout])
16
+ end
17
+
18
+ def valid?(domain)
19
+ return false if domain.nil? || domain.empty?
20
+
21
+ check_domain_records(domain)
22
+ end
23
+
24
+ private
25
+
26
+ def check_domain_records(domain)
27
+ if options[:check_mx]
28
+ return false unless dns_resolver.has_mx_record?(domain)
29
+ end
30
+
31
+ if options[:check_a]
32
+ return false unless dns_resolver.has_a_record?(domain)
33
+ end
34
+
35
+ true
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "email_address"
4
+
5
+ module EmailDomainChecker
6
+ class EmailAddressAdapter
7
+ attr_reader :email_address
8
+
9
+ def initialize(email)
10
+ @email_address = EmailAddress.new(email)
11
+ end
12
+
13
+ def valid?
14
+ email_address.valid?
15
+ end
16
+
17
+ def normalized
18
+ email_address.normal
19
+ end
20
+
21
+ def canonical
22
+ email_address.canonical
23
+ end
24
+
25
+ def redacted
26
+ email_address.redact
27
+ end
28
+
29
+ def to_s
30
+ email_address.to_s
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDomainChecker
4
+ class Normalizer
5
+ def self.normalize(raw)
6
+ return "" if raw.nil? || raw.to_s.strip.empty?
7
+
8
+ email_str = raw.to_s.strip
9
+ return "" if email_str.empty?
10
+
11
+ # Basic normalization: lowercase and IDN handling
12
+ local, domain = email_str.downcase.split("@", 2)
13
+ return email_str unless local && domain && !local.empty? && !domain.empty?
14
+
15
+ # IDN (Internationalized Domain Name) conversion
16
+ domain = idn_to_ascii(domain)
17
+ "#{local}@#{domain}"
18
+ end
19
+
20
+ def self.idn_to_ascii(domain)
21
+ # Simple IDN conversion using built-in methods
22
+ # For production, consider using the 'simpleidn' gem
23
+ begin
24
+ # Try to encode as IDN if it contains non-ASCII characters
25
+ if domain.match?(/[^\x00-\x7F]/)
26
+ # Fallback: return as-is if IDN conversion fails
27
+ # In production, use: SimpleIDN.to_ascii(domain)
28
+ domain
29
+ else
30
+ domain
31
+ end
32
+ rescue StandardError
33
+ domain
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDomainChecker
4
+ VERSION = "0.1.0"
5
+ end
6
+
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "email_domain_checker/version"
4
+ require_relative "email_domain_checker/config"
5
+ require_relative "email_domain_checker/normalizer"
6
+ require_relative "email_domain_checker/dns_resolver"
7
+ require_relative "email_domain_checker/domain_validator"
8
+ require_relative "email_domain_checker/email_address_adapter"
9
+ require_relative "email_domain_checker/checker"
10
+
11
+ module EmailDomainChecker
12
+ class Error < StandardError; end
13
+
14
+ # Convenience method for quick validation
15
+ def self.valid?(email, options = {})
16
+ Checker.new(email, options).valid?
17
+ end
18
+
19
+ # Convenience method for format validation only
20
+ def self.format_valid?(email)
21
+ Checker.new(email, validate_domain: false).format_valid?
22
+ end
23
+
24
+ # Convenience method for domain validation only
25
+ def self.domain_valid?(email, options = {})
26
+ Checker.new(email, validate_format: false, **options).domain_valid?
27
+ end
28
+
29
+ # Convenience method for normalization
30
+ def self.normalize(email)
31
+ Normalizer.normalize(email)
32
+ end
33
+
34
+ # Configure default options
35
+ def self.configure(options = {})
36
+ Config.configure(options)
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: email_domain_checker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Koki Tatematsu
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: email_address
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ description: A library to validate email addresses and check domain validity to prevent
69
+ mail server reputation degradation
70
+ email:
71
+ - koki.tatematsu@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rubocop.yml"
77
+ - CHANGELOG.md
78
+ - README.md
79
+ - Rakefile
80
+ - email_domain_checker.gemspec
81
+ - lib/email_domain_checker.rb
82
+ - lib/email_domain_checker/checker.rb
83
+ - lib/email_domain_checker/config.rb
84
+ - lib/email_domain_checker/dns_resolver.rb
85
+ - lib/email_domain_checker/domain_validator.rb
86
+ - lib/email_domain_checker/email_address_adapter.rb
87
+ - lib/email_domain_checker/normalizer.rb
88
+ - lib/email_domain_checker/version.rb
89
+ homepage: https://github.com/tatematsu-k/email_domain_checker
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ allowed_push_host: https://rubygems.org
94
+ homepage_uri: https://github.com/tatematsu-k/email_domain_checker
95
+ source_code_uri: https://github.com/tatematsu-k/email_domain_checker
96
+ changelog_uri: https://github.com/tatematsu-k/email_domain_checker/blob/main/CHANGELOG.md
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '3.0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.7.2
112
+ specification_version: 4
113
+ summary: Email address validation and domain checking library
114
+ test_files: []