mailshield 0.1.1 → 1.1

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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailshield
4
- VERSION = "0.1.1"
4
+ VERSION = "1.1"
5
5
  end
data/lib/mailshield.rb CHANGED
@@ -1,133 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "mailshield/version"
3
+ require_relative 'mailshield/version'
4
4
  require 'resolv'
5
5
  require 'csv'
6
+ require 'net/smtp'
6
7
 
7
8
  module MailShield
9
+ EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/.freeze
8
10
 
9
- class ValidationResult
10
- attr_reader :emails
11
-
12
- def initialize(emails)
13
- @emails = emails
14
- end
15
-
16
- def valid_emails
17
- emails.select { |email| email[:valid] }.map { |email| email[:email] }
18
- end
19
-
20
- def invalid_emails
21
- emails.reject { |email| email[:valid] }.map { |email| email[:email] }
22
- end
23
-
24
- def get_emails
25
- emails.map { |email| { email: email[:email], valid: email[:valid] } }
26
- end
27
- end
11
+ class ValidationError < StandardError; end
12
+ class InvalidFormatError < ValidationError; end
13
+ class DomainNotFoundError < ValidationError; end
14
+ class SPFError < ValidationError; end
15
+ class DMARCError < ValidationError; end
16
+ class SMTPError < ValidationError; end
28
17
 
29
18
  class << self
30
- TEMP_DOMAINS = %w[mailinator.com tempmail.com guerrillamail.com].freeze # Known temporary domains
31
-
19
+ attr_accessor :dns_cache, :smtp_cache
32
20
 
33
- # Main method to validate an email and return results in a hash
34
- def validate_email(email)
35
- result = {
36
- email: email,
37
- valid: true,
38
- issues: []
39
- }
40
-
41
- unless valid_email_format?(email)
42
- result[:valid] = false
43
- result[:issues] << "The email address format is invalid."
44
- return result
45
- end
21
+ def validate_email(email, verify_by_send: false)
22
+ reset_caches
46
23
 
47
24
  domain = extract_domain(email)
48
25
 
49
- if known_temp_domain?(domain)
50
- result[:valid] = false
51
- result[:issues] << "The email domain is known to be temporary or disposable."
52
- return result
53
- end
54
-
55
- mx_records = fetch_mx_records(domain)
56
- if mx_records.empty?
57
- result[:valid] = false
58
- result[:issues] << "The email domain does not have valid mail exchange records."
59
- return result
60
- elsif suspicious_mx_records?(mx_records)
61
- result[:valid] = false
62
- result[:issues] << "The email domain's mail exchange records suggest it might be used for temporary or disposable email services."
63
- return result
26
+ begin
27
+ validate_format!(email)
28
+ validate_domain!(domain)
29
+ validate_spf!(domain)
30
+ validate_dmarc!(domain)
31
+ validate_smtp!(email) if verify_by_send
32
+ rescue ValidationError => e
33
+ return { valid: false, issues: [e.message] }
64
34
  end
65
35
 
66
- unless spf_record?(domain)
67
- result[:valid] = false
68
- result[:issues] << "The email domain is missing important records that help confirm its authenticity."
69
- return result
70
- end
36
+ { valid: true, issues: [] }
37
+ end
71
38
 
72
- unless dmarc_record?(domain)
73
- result[:valid] = false
74
- result[:issues] << "The email domain is missing records that help protect against email fraud."
75
- return result
76
- end
77
39
 
78
- result
40
+ def verify_address(email)
41
+ smtp_verify_email(email)
79
42
  end
43
+ private
80
44
 
81
- def validate_emails_from_csv(input_file_path)
82
- email_results = []
83
-
84
- CSV.foreach(input_file_path, headers: true) do |row|
85
- email = row['email']
86
- validation_result = validate_email_for_csv(email)
87
- email_results << { email: email, valid: validation_result }
88
- end
45
+ def reset_caches
46
+ @dns_cache = {}
47
+ @smtp_cache = {}
48
+ end
89
49
 
90
- ValidationResult.new(email_results)
50
+ def validate_format!(email)
51
+ raise InvalidFormatError, 'The email address format is invalid.' unless valid_email_format?(email)
91
52
  end
92
53
 
93
- def validate_email_for_csv(email)
94
- return false unless valid_email_format?(email)
95
- domain = extract_domain(email)
54
+ def validate_domain!(domain)
55
+ raise DomainNotFoundError, 'Email Not Found.' if fetch_mx_records(domain).empty?
56
+ end
96
57
 
97
- return false if known_temp_domain?(domain)
98
- return false unless spf_record?(domain)
99
- return false unless dmarc_record?(domain)
58
+ def validate_spf!(domain)
59
+ raise SPFError, 'Temporary / Disposable Email' unless spf_record?(domain)
60
+ end
100
61
 
101
- true
62
+ def validate_dmarc!(domain)
63
+ raise DMARCError, 'Temporary / Disposable Email' unless dmarc_record?(domain)
102
64
  end
103
65
 
104
- private
66
+ def validate_smtp!(email)
67
+ raise SMTPError, 'Email Address Not Found' unless smtp_verify_email(email)
68
+ end
105
69
 
106
70
  def extract_domain(email)
107
71
  email.split('@').last.downcase
108
72
  end
109
73
 
110
74
  def valid_email_format?(email)
111
- email_regex = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
112
- !!(email =~ email_regex)
113
- end
114
-
115
- def known_temp_domain?(domain)
116
- TEMP_DOMAINS.include?(domain)
75
+ EMAIL_REGEX.match?(email)
117
76
  end
118
77
 
119
78
  def fetch_mx_records(domain)
120
- Resolv::DNS.open do |dns|
121
- mx_records = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
122
- mx_records.map(&:exchange).map(&:to_s)
79
+ dns_cache[domain] ||= begin
80
+ Resolv::DNS.open do |dns|
81
+ dns.getresources(domain, Resolv::DNS::Resource::IN::MX).map(&:exchange).map(&:to_s)
82
+ end
83
+ rescue Resolv::ResolvError
84
+ []
123
85
  end
124
- rescue Resolv::ResolvError
125
- []
126
- end
127
-
128
- def suspicious_mx_records?(mx_records)
129
- suspicious_patterns = [/mailinator/, /tempmail/, /guerrillamail/]
130
- mx_records.any? { |mx| suspicious_patterns.any? { |pattern| mx.match?(pattern) } }
131
86
  end
132
87
 
133
88
  def spf_record?(domain)
@@ -141,27 +96,36 @@ module MailShield
141
96
  end
142
97
 
143
98
  def fetch_txt_records(domain)
144
- Resolv::DNS.open do |dns|
145
- records = dns.getresources(domain, Resolv::DNS::Resource::IN::TXT)
146
- records.map(&:data)
99
+ dns_cache[domain] ||= begin
100
+ Resolv::DNS.open do |dns|
101
+ dns.getresources(domain, Resolv::DNS::Resource::IN::TXT).map(&:data)
102
+ end
103
+ rescue Resolv::ResolvError
104
+ []
147
105
  end
148
- rescue Resolv::ResolvError
149
- []
150
106
  end
151
- end
152
- end
153
107
 
108
+ def smtp_verify_email(email)
109
+ domain = extract_domain(email)
110
+ smtp_server = smtp_cache[domain] ||= get_smtp_server(domain)
111
+
112
+ return false unless smtp_server
113
+
114
+ begin
115
+ Net::SMTP.start(smtp_server, 25, 'localhost') do |smtp|
116
+ smtp.helo('localhost')
117
+ smtp.mailfrom('test@example.com')
118
+ response = smtp.rcptto(email)
119
+ response == '250 OK'
120
+ end
121
+ rescue Net::SMTPFatalError, Net::SMTPServerBusy, Net::SMTPSyntaxError, Errno::ECONNREFUSED => e
122
+ false
123
+ end
124
+ end
154
125
 
155
- # SPF Record Explanation:
156
- #
157
- # SPF (Sender Policy Framework): A mechanism that helps email servers verify that an email claiming to come from a specific domain is actually sent by an authorized mail server.
158
- # This prevents spammers from sending messages with forged "From" addresses.
159
- #
160
- # DMARC Record Explanation:
161
- #
162
- # DMARC (Domain-based Message Authentication, Reporting, and Conformance): A protocol that builds on SPF and DKIM (DomainKeys Identified Mail) to help email domain owners protect their domain from being used in email spoofing.
163
- # It provides a way for domain owners to publish policies about their email authentication practices and how receiving mail servers should enforce them.
164
- #
165
- # References:
166
- # https://www.dkim.org/
167
- # https://www.cloudflare.com/en-gb/learning/dns/dns-records/dns-spf-record/
126
+ def get_smtp_server(domain)
127
+ mx_records = fetch_mx_records(domain)
128
+ mx_records.first
129
+ end
130
+ end
131
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailshield
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: '1.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - jana
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-25 00:00:00.000000000 Z
11
+ date: 2024-08-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: MailShield helps secure your application by identifying temporary or
14
14
  disposable email domains.
@@ -19,6 +19,9 @@ extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - lib/mailshield.rb
22
+ - lib/mailshield/docs/mailshield.png
23
+ - lib/mailshield/secure_email_validator.rb
24
+ - lib/mailshield/temp_domains.yml
22
25
  - lib/mailshield/version.rb
23
26
  homepage: https://github.com/janarthanan-shanmugam/mailshield
24
27
  licenses:
@@ -43,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
46
  - !ruby/object:Gem::Version
44
47
  version: '0'
45
48
  requirements: []
46
- rubygems_version: 3.5.11
49
+ rubygems_version: 3.5.18
47
50
  signing_key:
48
51
  specification_version: 4
49
52
  summary: A gem to detect temporary or disposable email domains.