deliverable 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6939d9e02d7269dc2704b727f9aaf27b2374e271d88c03e48c126bfe6695132a
4
- data.tar.gz: e0eca76ecdfc1478bfe3c507555c4b59054ac898a658499a573b86478f87c684
3
+ metadata.gz: 789cda6bdfbcd5cbd6f552775c78227c1901fba7c7f792e97e9d87f1f0f0411e
4
+ data.tar.gz: 5d63aee7ce38ad7b098022909a025d688624eef3bfec1f7418085086eac2eb9b
5
5
  SHA512:
6
- metadata.gz: 101133007279b3c6610dcad60d8c9dcb0a86a8ff9d5f7aa215d29cbe3cba1d0daf53bcd75f32afdc07ac56e154f019fd537dc4b6b15a7f2670cacaa9ae474c58
7
- data.tar.gz: d7af7df7bb8ac409e211103eca6851ab63142cc9dc38fece9ae7cd9e3b2d00fea250466c97e8ea69ebc9702712bece6f1e5f0b182b2e42e971bc6adc862c7aba
6
+ metadata.gz: b1ec53c65f23b78024ef0b4ef4741f8cd55f57f3ab85405ae1376fcb6988220985a1655bcf36ca18996bcfa8e30f63048d6b2a010a9149cc19e8047a0a2a900d
7
+ data.tar.gz: e663054eba78bfe71a4638e1e07bd098e17850d32d2de5cdebe96bad7721f897c5bcbc05976126a54ed46d52e0d7f0e74baa6e6f7be821f15d19483492faeb98
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Add `smtp_response` to the result: the SMTP code and message returned by
6
+ the destination MX (e.g. `{ code: "421", message: "Greylisted, try again" }`).
7
+ `nil` when no SMTP probe was made or no response was received.
8
+ - Add `retryable` boolean to the result. `true` when the verifier hit a
9
+ transient signal (4xx response, server-busy, timeout, connection refused, or
10
+ a transient DNS error) and the result is `risky`. Callers can use this to
11
+ decide whether to enqueue a follow-up verification.
12
+
3
13
  ## 0.1.0
4
14
 
5
15
  - Initial release.
@@ -1,8 +1,8 @@
1
1
  module Deliverable
2
2
  class Result
3
- attr_reader :email, :score, :score_details, :errors, :warnings, :checks
3
+ attr_reader :email, :score, :score_details, :errors, :warnings, :checks, :smtp_response
4
4
 
5
- def initialize(email:, score:, score_details:, classification:, errors:, warnings:, checks:)
5
+ def initialize(email:, score:, score_details:, classification:, errors:, warnings:, checks:, smtp_response: nil, retryable: false)
6
6
  @email = email
7
7
  @score = score
8
8
  @score_details = score_details
@@ -10,6 +10,8 @@ module Deliverable
10
10
  @errors = errors
11
11
  @warnings = warnings
12
12
  @checks = checks
13
+ @smtp_response = smtp_response
14
+ @retryable = retryable
13
15
  end
14
16
 
15
17
  def valid?
@@ -20,6 +22,11 @@ module Deliverable
20
22
  @classification
21
23
  end
22
24
 
25
+ def retryable?
26
+ @retryable
27
+ end
28
+ alias_method :retryable, :retryable?
29
+
23
30
  def to_h
24
31
  {
25
32
  email: @email,
@@ -29,6 +36,8 @@ module Deliverable
29
36
  score_details: @score_details,
30
37
  errors: @errors,
31
38
  warnings: @warnings,
39
+ retryable: @retryable,
40
+ smtp_response: @smtp_response,
32
41
  checks: @checks
33
42
  }
34
43
  end
@@ -44,18 +44,23 @@ module Deliverable
44
44
  accepts_any_email: false
45
45
  }
46
46
  @accepts_any_email = nil
47
+ @smtp_response = nil
48
+ @smtp_retryable = false
47
49
  end
48
50
 
49
51
  def call
50
52
  ok = run_checks
51
53
  score = calculate_score
54
+ cls = classification(ok)
52
55
  Result.new(
53
56
  email: @email,
54
57
  score: score,
55
58
  score_details: @details.dup,
56
- classification: classification(ok),
59
+ classification: cls,
57
60
  errors: @errors,
58
61
  warnings: @warnings,
62
+ smtp_response: @smtp_response,
63
+ retryable: retryable?(cls),
59
64
  checks: {
60
65
  syntax: @details[:syntax_valid],
61
66
  mx_record: @details[:mx_verified],
@@ -225,18 +230,32 @@ module Deliverable
225
230
  smtp.start(@sender_domain) do |conn|
226
231
  conn.mailfrom(@sender_email)
227
232
  begin
228
- conn.rcptto(recipient)
233
+ response = conn.rcptto(recipient)
234
+ record_response(response.status, response.message) if response.respond_to?(:status)
229
235
  return :verified
230
236
  rescue Net::SMTPFatalError => e
237
+ record_error(e)
231
238
  classify_fatal(e.message)
232
- rescue Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPAuthenticationError
239
+ rescue Net::SMTPServerBusy => e
240
+ record_error(e, retryable: true)
241
+ return :assumed
242
+ rescue Net::SMTPSyntaxError, Net::SMTPAuthenticationError => e
243
+ record_error(e)
233
244
  return :assumed
234
245
  end
235
246
  end
236
247
  end
237
- rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH
248
+ rescue Timeout::Error => e
249
+ record_transient(e.message)
250
+ :connection_failed
251
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH => e
252
+ record_transient(e.message)
238
253
  :connection_failed
239
- rescue Net::SMTPAuthenticationError, Net::SMTPServerBusy
254
+ rescue Net::SMTPAuthenticationError => e
255
+ record_error(e)
256
+ :assumed
257
+ rescue Net::SMTPServerBusy => e
258
+ record_error(e, retryable: true)
240
259
  :assumed
241
260
  rescue StandardError => e
242
261
  log "SMTP probe failed for #{recipient} on #{mail_server}:#{port}: #{e.message}"
@@ -244,18 +263,40 @@ module Deliverable
244
263
  end
245
264
 
246
265
  def classify_fatal(message)
247
- msg = message.to_s.downcase
248
- if msg.include?("550") || msg.include?("551") ||
249
- msg.include?("user unknown") || msg.include?("recipient unknown") ||
250
- msg.include?("mailbox unavailable")
266
+ code = extract_code(message)
267
+ case code&.[](0)
268
+ when "5"
251
269
  :invalid
252
- elsif msg.include?("421") || msg.include?("450") || msg.include?("451") || msg.include?("temporary")
270
+ when "4"
271
+ @smtp_retryable = true
253
272
  :assumed
254
273
  else
274
+ if message.to_s.downcase.include?("temporary")
275
+ @smtp_retryable = true
276
+ end
255
277
  :assumed
256
278
  end
257
279
  end
258
280
 
281
+ def record_response(code, message)
282
+ @smtp_response = { code: code.to_s, message: message.to_s.strip }
283
+ end
284
+
285
+ def record_error(err, retryable: false)
286
+ code = extract_code(err.message)
287
+ @smtp_response = { code: code, message: err.message.to_s.strip }
288
+ @smtp_retryable = true if retryable || code&.start_with?("4")
289
+ end
290
+
291
+ def record_transient(message)
292
+ @smtp_response = { code: nil, message: message.to_s.strip }
293
+ @smtp_retryable = true
294
+ end
295
+
296
+ def extract_code(message)
297
+ message.to_s[/\b([45]\d{2})\b/, 1]
298
+ end
299
+
259
300
  def check_catch_all(mail_server)
260
301
  domain = extract_domain
261
302
  return unless domain
@@ -367,6 +408,11 @@ module Deliverable
367
408
  "valid"
368
409
  end
369
410
 
411
+ def retryable?(cls)
412
+ return false unless cls == "risky"
413
+ @smtp_retryable || @details[:dns_failed_assumption]
414
+ end
415
+
370
416
  def log(message)
371
417
  @logger&.warn(message)
372
418
  end
@@ -1,3 +1,3 @@
1
1
  module Deliverable
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deliverable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Strömberg
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-01 00:00:00.000000000 Z
11
+ date: 2026-05-11 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: valid_email2
@@ -83,6 +84,7 @@ metadata:
83
84
  homepage_uri: https://github.com/peopledb/deliverable
84
85
  source_code_uri: https://github.com/peopledb/deliverable
85
86
  changelog_uri: https://github.com/peopledb/deliverable/blob/main/CHANGELOG.md
87
+ post_install_message:
86
88
  rdoc_options: []
87
89
  require_paths:
88
90
  - lib
@@ -97,7 +99,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
99
  - !ruby/object:Gem::Version
98
100
  version: '0'
99
101
  requirements: []
100
- rubygems_version: 3.7.2
102
+ rubygems_version: 3.5.11
103
+ signing_key:
101
104
  specification_version: 4
102
105
  summary: 'Honest email verification: syntax, MX, SMTP RCPT, and catch-all detection.'
103
106
  test_files: []