email_address 0.1.2 → 0.2.4

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,6 +1,5 @@
1
- require 'simpleidn'
2
- require 'resolv'
3
- require 'netaddr'
1
+ require "simpleidn"
2
+ require "resolv"
4
3
 
5
4
  module EmailAddress
6
5
  ##############################################################################
@@ -32,16 +31,16 @@ module EmailAddress
32
31
  class Host
33
32
  attr_reader :host_name
34
33
  attr_accessor :dns_name, :domain_name, :registration_name,
35
- :tld, :tld2, :subdomains, :ip_address, :config, :provider,
36
- :comment
34
+ :tld, :tld2, :subdomains, :ip_address, :config, :provider,
35
+ :comment, :error_message, :reason, :locale
37
36
  MAX_HOST_LENGTH = 255
38
37
 
39
38
  # Sometimes, you just need a Regexp...
40
- DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: \-{1,2} | \.) [\p{L}\p{N}]+ )*/x
39
+ DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: -{1,2} | \.) [\p{L}\p{N}]+ )*/x
41
40
 
42
41
  # The IPv4 and IPv6 were lifted from Resolv::IPv?::Regex and tweaked to not
43
42
  # \A...\z anchor at the edges.
44
- IPv6_HOST_REGEX = /\[IPv6:
43
+ IPV6_HOST_REGEX = /\[IPv6:
45
44
  (?: (?:(?x-mi:
46
45
  (?:[0-9A-Fa-f]{1,4}:){7}
47
46
  [0-9A-Fa-f]{1,4}
@@ -60,7 +59,7 @@ module EmailAddress
60
59
  (?: \d+)\.(?: \d+)\.(?: \d+)\.(?: \d+)
61
60
  )))\]/ix
62
61
 
63
- IPv4_HOST_REGEX = /\[((?x-mi:0
62
+ IPV4_HOST_REGEX = /\[((?x-mi:0
64
63
  |1(?:[0-9][0-9]?)?
65
64
  |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
66
65
  |[3-9][0-9]?))\.((?x-mi:0
@@ -79,49 +78,50 @@ module EmailAddress
79
78
 
80
79
  # Matches Host forms: DNS name, IPv4, or IPv6 formats
81
80
  STANDARD_HOST_REGEX = /\A (?: #{DNS_HOST_REGEX}
82
- | #{IPv4_HOST_REGEX} | #{IPv6_HOST_REGEX}) \z/ix
81
+ | #{IPV4_HOST_REGEX} | #{IPV6_HOST_REGEX}) \z/ix
83
82
 
84
83
  # host name -
85
84
  # * host type - :email for an email host, :mx for exchanger host
86
- def initialize(host_name, config={})
87
- @original = host_name ||= ''
85
+ def initialize(host_name, config = {}, locale = "en")
86
+ @original = host_name ||= ""
87
+ @locale = locale
88
88
  config[:host_type] ||= :email
89
- @config = config
89
+ @config = config.is_a?(Hash) ? Config.new(config) : config
90
+ @error = @error_message = nil
90
91
  parse(host_name)
91
92
  end
92
93
 
93
94
  # Returns the String representation of the host name (or IP)
94
95
  def name
95
- if self.ipv4?
96
- "[#{self.ip_address}]"
97
- elsif self.ipv6?
98
- "[IPv6:#{self.ip_address}]"
96
+ if ipv4?
97
+ "[#{ip_address}]"
98
+ elsif ipv6?
99
+ "[IPv6:#{ip_address}]"
99
100
  elsif @config[:host_encoding] && @config[:host_encoding] == :unicode
100
- ::SimpleIDN.to_unicode(self.host_name)
101
+ ::SimpleIDN.to_unicode(host_name)
101
102
  else
102
- self.dns_name
103
+ dns_name
103
104
  end
104
105
  end
105
- alias :to_s :name
106
+ alias_method :to_s, :name
106
107
 
107
108
  # The canonical host name is the simplified, DNS host name
108
109
  def canonical
109
- self.dns_name
110
+ dns_name
110
111
  end
111
112
 
112
113
  # Returns the munged version of the name, replacing everything after the
113
114
  # initial two characters with "*****" or the configured "munge_string".
114
115
  def munge
115
- self.host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
116
+ host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
116
117
  end
117
118
 
118
119
  ############################################################################
119
120
  # Parsing
120
121
  ############################################################################
121
122
 
122
-
123
123
  def parse(host) # :nodoc:
124
- host = self.parse_comment(host)
124
+ host = parse_comment(host)
125
125
 
126
126
  if host =~ /\A\[IPv6:(.+)\]/i
127
127
  self.ip_address = $1
@@ -143,62 +143,96 @@ module EmailAddress
143
143
  end
144
144
 
145
145
  def host_name=(name)
146
- @host_name = name = name.strip.downcase.gsub(' ', '').gsub(/\(.*\)/, '')
147
- @dns_name = ::SimpleIDN.to_ascii(self.host_name)
146
+ name = fully_qualified_domain_name(name.downcase)
147
+ @host_name = name
148
+ if @config[:host_remove_spaces]
149
+ @host_name = @host_name.delete(" ")
150
+ end
151
+ @dns_name = if /[^[:ascii:]]/.match?(host_name)
152
+ ::SimpleIDN.to_ascii(host_name)
153
+ else
154
+ host_name
155
+ end
148
156
 
149
157
  # Subdomain only (root@localhost)
150
- if name.index('.').nil?
158
+ if name.index(".").nil?
151
159
  self.subdomains = name
152
160
 
153
161
  # Split sub.domain from .tld: *.com, *.xx.cc, *.cc
154
162
  elsif name =~ /\A(.+)\.(\w{3,10})\z/ ||
155
- name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
156
- name =~ /\A(.+)\.(\w\w)\z/
163
+ name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
164
+ name =~ /\A(.+)\.(\w\w)\z/
157
165
 
158
166
  sub_and_domain, self.tld2 = [$1, $2] # sub+domain, com || co.uk
159
- self.tld = self.tld2.sub(/\A.+\./, '') # co.uk => uk
167
+ self.tld = tld2.sub(/\A.+\./, "") # co.uk => uk
160
168
  if sub_and_domain =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld2]
161
- self.subdomains = $1
169
+ self.subdomains = $1
162
170
  self.registration_name = $2
163
171
  else
164
172
  self.registration_name = sub_and_domain
165
- #self.domain_name = sub_and_domain + '.' + self.tld2
173
+ # self.domain_name = sub_and_domain + '.' + self.tld2
166
174
  end
167
- self.domain_name = self.registration_name + '.' + self.tld2
168
- self.find_provider
175
+ self.domain_name = registration_name + "." + tld2
176
+ find_provider
177
+ else # Bad format
178
+ self.subdomains = self.tld = self.tld2 = ""
179
+ self.domain_name = self.registration_name = name
169
180
  end
170
181
  end
171
182
 
172
- def find_provider # :nodoc:
173
- return self.provider if self.provider
174
-
175
- EmailAddress::Config.providers.each do |provider, config|
176
- if config[:host_match] && self.matches?(config[:host_match])
177
- return self.set_provider(provider, config)
183
+ def fully_qualified_domain_name(host_part)
184
+ dn = @config[:address_fqdn_domain]
185
+ if !dn
186
+ if (host_part.nil? || host_part <= " ") && @config[:host_local] && @config[:host_auto_append]
187
+ "localhost"
188
+ else
189
+ host_part
178
190
  end
191
+ elsif host_part.nil? || host_part <= " "
192
+ dn
193
+ elsif !host_part.include?(".")
194
+ host_part + "." + dn
195
+ else
196
+ host_part
179
197
  end
198
+ end
199
+
200
+ # True if host is hosted at the provider, not a public provider host name
201
+ def hosted_service?
202
+ return false unless registration_name
203
+ find_provider
204
+ return false unless config[:host_match]
205
+ !matches?(config[:host_match])
206
+ end
180
207
 
181
- return self.set_provider(:default) unless self.dns_enabled?
208
+ def find_provider # :nodoc:
209
+ return provider if provider
182
210
 
183
- provider = self.exchangers.provider
184
- if provider != :default
185
- self.set_provider(provider,
186
- EmailAddress::Config.provider(provider))
211
+ Config.providers.each do |provider, config|
212
+ if config[:host_match] && matches?(config[:host_match])
213
+ return set_provider(provider, config)
214
+ end
187
215
  end
188
216
 
189
- self.provider ||= self.set_provider(:default)
217
+ return set_provider(:default) unless dns_enabled?
218
+
219
+ self.provider ||= set_provider(:default)
190
220
  end
191
221
 
192
- def set_provider(name, provider_config={}) # :nodoc:
193
- self.config = EmailAddress::Config.all_settings(provider_config, @config)
194
- self.provider = name
222
+ def set_provider(name, provider_config = {}) # :nodoc:
223
+ config.configure(provider_config)
224
+ @provider = name
195
225
  end
196
226
 
197
227
  # Returns a hash of the parts of the host name after parsing.
198
228
  def parts
199
- { host_name:self.host_name, dns_name:self.dns_name, subdomain:self.subdomains,
200
- registration_name:self.registration_name, domain_name:self.domain_name,
201
- tld2:self.tld2, tld:self.tld, ip_address:self.ip_address }
229
+ {host_name: host_name, dns_name: dns_name, subdomain: subdomains,
230
+ registration_name: registration_name, domain_name: domain_name,
231
+ tld2: tld2, tld: tld, ip_address: ip_address}
232
+ end
233
+
234
+ def hosted_provider
235
+ Exchanger.cached(dns_name).provider
202
236
  end
203
237
 
204
238
  ############################################################################
@@ -207,19 +241,19 @@ module EmailAddress
207
241
 
208
242
  # Is this a fully-qualified domain name?
209
243
  def fqdn?
210
- self.tld ? true : false
244
+ tld ? true : false
211
245
  end
212
246
 
213
247
  def ip?
214
- self.ip_address.nil? ? false : true
248
+ !!ip_address
215
249
  end
216
250
 
217
251
  def ipv4?
218
- self.ip? && self.ip_address.include?(".")
252
+ ip? && ip_address.include?(".")
219
253
  end
220
254
 
221
255
  def ipv6?
222
- self.ip? && self.ip_address.include?(":")
256
+ ip? && ip_address.include?(":")
223
257
  end
224
258
 
225
259
  ############################################################################
@@ -237,25 +271,25 @@ module EmailAddress
237
271
  rules = Array(rules)
238
272
  return false if rules.empty?
239
273
  rules.each do |rule|
240
- return rule if rule == self.domain_name || rule == self.dns_name
274
+ return rule if rule == domain_name || rule == dns_name
241
275
  return rule if registration_name_matches?(rule)
242
276
  return rule if tld_matches?(rule)
243
277
  return rule if domain_matches?(rule)
244
278
  return rule if self.provider && provider_matches?(rule)
245
- return rule if self.ip_matches?(rule)
279
+ return rule if ip_matches?(rule)
246
280
  end
247
281
  false
248
282
  end
249
283
 
250
284
  # Does "example." match any tld?
251
285
  def registration_name_matches?(rule)
252
- self.registration_name + '.' == rule ? true : false
286
+ rule == "#{registration_name}."
253
287
  end
254
288
 
255
289
  # Does "sub.example.com" match ".com" and ".example.com" top level names?
256
290
  # Matches TLD (uk) or TLD2 (co.uk)
257
291
  def tld_matches?(rule)
258
- rule.match(/\A\.(.+)\z/) && ($1 == self.tld || $1 == self.tld2) ? true : false
292
+ rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) # ? true : false
259
293
  end
260
294
 
261
295
  def provider_matches?(rule)
@@ -266,24 +300,17 @@ module EmailAddress
266
300
  # Requires optionally starts with a "@".
267
301
  def domain_matches?(rule)
268
302
  rule = $1 if rule =~ /\A@(.+)/
269
- return rule if File.fnmatch?(rule, self.domain_name)
270
- return rule if File.fnmatch?(rule, self.dns_name)
303
+ return rule if domain_name && File.fnmatch?(rule, domain_name)
304
+ return rule if dns_name && File.fnmatch?(rule, dns_name)
271
305
  false
272
306
  end
273
307
 
274
308
  # True if the host is an IP Address form, and that address matches
275
309
  # the passed CIDR string ("10.9.8.0/24" or "2001:..../64")
276
310
  def ip_matches?(cidr)
277
- return false unless self.ip_address
278
- return cidr if !cidr.include?("/") && cidr == self.ip_address
279
-
280
- c = NetAddr::CIDR.create(cidr)
281
- if cidr.include?(":") && self.ip_address.include?(":")
282
- return cidr if c.matches?(self.ip_address)
283
- elsif cidr.include?(".") && self.ip_address.include?(".")
284
- return cidr if c.matches?(self.ip_address)
285
- end
286
- false
311
+ return false unless ip_address
312
+ net = IPAddr.new(cidr)
313
+ net.include?(IPAddr.new(ip_address))
287
314
  end
288
315
 
289
316
  ############################################################################
@@ -292,45 +319,50 @@ module EmailAddress
292
319
 
293
320
  # True if the :dns_lookup setting is enabled
294
321
  def dns_enabled?
295
- EmailAddress::Config.setting(:dns_lookup).equal?(:off) ? false : true
296
- end
297
-
298
- # True if the host name has a DNS A Record
299
- def has_dns_a_record?
300
- dns_a_record.size > 0 ? true : false
322
+ return false if @config[:dns_lookup] == :off
323
+ return false if @config[:host_validation] == :syntax
324
+ true
301
325
  end
302
326
 
303
327
  # Returns: [official_hostname, alias_hostnames, address_family, *address_list]
304
328
  def dns_a_record
305
- @_dns_a_record ||= Socket.gethostbyname(self.dns_name)
329
+ @_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
330
+ @_dns_a_record ||= Addrinfo.getaddrinfo(dns_name, 80) # Port 80 for A rec, 25 for MX
306
331
  rescue SocketError # not found, but could also mean network not work
307
332
  @_dns_a_record ||= []
308
333
  end
309
334
 
310
- # Returns an array of EmailAddress::Exchanger hosts configured in DNS.
335
+ # Returns an array of Exchanger hosts configured in DNS.
311
336
  # The array will be empty if none are configured.
312
337
  def exchangers
313
- return nil if @config[:host_type] != :email || !self.dns_enabled?
314
- @_exchangers ||= EmailAddress::Exchanger.cached(self.dns_name)
338
+ # return nil if @config[:host_type] != :email || !self.dns_enabled?
339
+ @_exchangers ||= Exchanger.cached(dns_name, @config)
315
340
  end
316
341
 
317
342
  # Returns a DNS TXT Record
318
- def txt(alternate_host=nil)
343
+ def txt(alternate_host = nil)
344
+ return nil unless dns_enabled?
319
345
  Resolv::DNS.open do |dns|
320
- records = dns.getresources(alternate_host || self.dns_name,
321
- Resolv::DNS::Resource::IN::TXT)
346
+ dns.timeouts = @config[:dns_timeout] if @config[:dns_timeout]
347
+ records = begin
348
+ dns.getresources(alternate_host || dns_name,
349
+ Resolv::DNS::Resource::IN::TXT)
350
+ rescue Resolv::ResolvTimeout
351
+ []
352
+ end
353
+
322
354
  records.empty? ? nil : records.map(&:data).join(" ")
323
355
  end
324
356
  end
325
357
 
326
358
  # Parses TXT record pairs into a hash
327
- def txt_hash(alternate_host=nil)
359
+ def txt_hash(alternate_host = nil)
328
360
  fields = {}
329
- record = self.txt(alternate_host)
361
+ record = txt(alternate_host)
330
362
  return fields unless record
331
363
 
332
364
  record.split(/\s*;\s*/).each do |pair|
333
- (n,v) = pair.split(/\s*=\s*/)
365
+ (n, v) = pair.split(/\s*=\s*/)
334
366
  fields[n.to_sym] = v
335
367
  end
336
368
  fields
@@ -339,7 +371,7 @@ module EmailAddress
339
371
  # Returns a hash of the domain's DMARC (https://en.wikipedia.org/wiki/DMARC)
340
372
  # settings.
341
373
  def dmarc
342
- self.dns_name ? self.txt_hash("_dmarc." + self.dns_name) : {}
374
+ dns_name ? txt_hash("_dmarc." + dns_name) : {}
343
375
  end
344
376
 
345
377
  ############################################################################
@@ -347,34 +379,124 @@ module EmailAddress
347
379
  ############################################################################
348
380
 
349
381
  # Returns true if the host name is valid according to the current configuration
350
- def valid?(rule=@config[:dns_lookup]||:mx)
351
- if self.provider != :default # well known
382
+ def valid?(rules = {})
383
+ host_validation = rules[:host_validation] || @config[:host_validation] || :mx
384
+ dns_lookup = rules[:dns_lookup] || host_validation
385
+ self.error_message = nil
386
+ if host_name && !host_name.empty? && !@config[:host_size].include?(host_name.size)
387
+ return set_error(:invalid_host)
388
+ end
389
+ if ip_address
390
+ valid_ip?
391
+ elsif !valid_format?
392
+ false
393
+ elsif dns_lookup == :connect
394
+ valid_mx? && connect
395
+ elsif dns_lookup == :mx
396
+ valid_mx?
397
+ elsif dns_lookup == :a
398
+ valid_dns?
399
+ else
352
400
  true
353
- elsif self.ip_address
354
- @config[:host_allow_ip] && self.valid_ip?
355
- elsif rule == :mx
356
- self.exchangers.mx_ips.size > 0
357
- elsif rule == :a
358
- self.has_dns_a_record?
359
- elsif rule == :off
360
- self.to_s.size <= MAX_HOST_LENGTH
401
+ end
402
+ end
403
+
404
+ # True if the host name has a DNS A Record
405
+ def valid_dns?
406
+ return true unless dns_enabled?
407
+ dns_a_record.size > 0 || set_error(:domain_unknown)
408
+ end
409
+
410
+ # True if the host name has valid MX servers configured in DNS
411
+ def valid_mx?
412
+ return true unless dns_enabled?
413
+ if exchangers.nil?
414
+ set_error(:domain_unknown)
415
+ elsif exchangers.mx_ips.size > 0
416
+ if localhost? && !@config[:host_local]
417
+ set_error(:domain_no_localhost)
418
+ else
419
+ true
420
+ end
421
+ elsif @config[:dns_timeout].nil? && valid_dns?
422
+ set_error(:domain_does_not_accept_email)
361
423
  else
362
- false
424
+ set_error(:domain_unknown)
363
425
  end
364
426
  end
365
427
 
428
+ # True if the host_name passes Regular Expression match and size limits.
429
+ def valid_format?
430
+ if host_name =~ CANONICAL_HOST_REGEX && to_s.size <= MAX_HOST_LENGTH
431
+ if localhost?
432
+ return @config[:host_local] ? true : set_error(:domain_no_localhost)
433
+ end
434
+
435
+ return true if !@config[:host_fqdn]
436
+ return true if host_name.include?(".") # require FQDN
437
+ end
438
+ set_error(:domain_invalid)
439
+ end
440
+
366
441
  # Returns true if the IP address given in that form of the host name
367
442
  # is a potentially valid IP address. It does not check if the address
368
443
  # is reachable.
369
444
  def valid_ip?
370
- if self.ip_address.nil?
371
- false
372
- elsif self.ip_address.include?(":")
373
- self.ip_address =~ Resolv::IPv6::Regex
374
- elsif self.ip_address.include?(".")
375
- self.ip_address =~ Resolv::IPv4::Regex
445
+ if !@config[:host_allow_ip]
446
+ bool = set_error(:ip_address_forbidden)
447
+ elsif ip_address.include?(":")
448
+ bool = ip_address.match(Resolv::IPv6::Regex) ? true : set_error(:ipv6_address_invalid)
449
+ elsif ip_address.include?(".")
450
+ bool = ip_address.match(Resolv::IPv4::Regex) ? true : set_error(:ipv4_address_invalid)
376
451
  end
452
+ if bool && (localhost? && !@config[:host_local])
453
+ bool = set_error(:ip_address_no_localhost)
454
+ end
455
+ bool
456
+ end
457
+
458
+ def localhost?
459
+ return true if host_name == "localhost"
460
+ return false unless ip_address
461
+ IPAddr.new(ip_address).loopback?
377
462
  end
378
463
 
464
+ # Connects to host to test it can receive email. This should NOT be performed
465
+ # as an email address check, but is provided to assist in problem resolution.
466
+ # If you abuse this, you *could* be blocked by the ESP.
467
+ #
468
+ # timeout is the number of seconds to wait before timing out the request and
469
+ # returns false as the connection was unsuccessful.
470
+ #
471
+ # > NOTE: As of Ruby 3.1, Net::SMTP was moved from the standard library to the
472
+ # > 'net-smtp' gem. In order to avoid adding that dependency for this *experimental*
473
+ # > feature, please add the gem to your Gemfile and require it to use this feature.
474
+ def connect(timeout = nil)
475
+ smtp = Net::SMTP.new(host_name || ip_address)
476
+ smtp.open_timeout = timeout || @config[:host_timeout]
477
+ smtp.start(@config[:helo_name] || "localhost")
478
+ smtp.finish
479
+ true
480
+ rescue Net::SMTPFatalError => e
481
+ set_error(:server_not_available, e.to_s)
482
+ rescue SocketError => e
483
+ set_error(:server_not_available, e.to_s)
484
+ rescue Net::OpenTimeout => e
485
+ set_error(:server_not_available, e.to_s)
486
+ ensure
487
+ smtp.finish if smtp&.started?
488
+ end
489
+
490
+ def set_error(err, reason = nil)
491
+ @error = err
492
+ @reason = reason
493
+ @error_message = Config.error_message(err, locale)
494
+ false
495
+ end
496
+
497
+ # The inverse of valid? -- Returns nil (falsey) if valid, otherwise error message
498
+ def error
499
+ valid? ? nil : @error_message
500
+ end
379
501
  end
380
502
  end