deliverable 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6939d9e02d7269dc2704b727f9aaf27b2374e271d88c03e48c126bfe6695132a
4
- data.tar.gz: e0eca76ecdfc1478bfe3c507555c4b59054ac898a658499a573b86478f87c684
3
+ metadata.gz: e68d3c1f29cb9fac64f7185d0b4026f6d8f9bcf5a6e55d9e7d89b3b3ab126ae4
4
+ data.tar.gz: e6ca8b392fef7e5289586a2c774684e31b4330b23b2c20dea30571ed256331b5
5
5
  SHA512:
6
- metadata.gz: 101133007279b3c6610dcad60d8c9dcb0a86a8ff9d5f7aa215d29cbe3cba1d0daf53bcd75f32afdc07ac56e154f019fd537dc4b6b15a7f2670cacaa9ae474c58
7
- data.tar.gz: d7af7df7bb8ac409e211103eca6851ab63142cc9dc38fece9ae7cd9e3b2d00fea250466c97e8ea69ebc9702712bece6f1e5f0b182b2e42e971bc6adc862c7aba
6
+ metadata.gz: 72cc381a74af672304ae18c4d5523dd85ed5174e33638a4958f709da2cbe9f5fd3dc34defa2ce02842a59c11e3c3862e8ddaeb15dba4f587d5bd0911a2337b3c
7
+ data.tar.gz: d710009b9129b3dfc8014bebc374edf586ea3c3ce0aa0ae1390fa14f8fb274022bbeac79f6e14cd4d660dd139649918ee1eab93c7749add82c204361182a1eab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Fix: the catch-all probe was overwriting `smtp_response` and
6
+ `retryable` on the Result. A verification that returned a clean 250
7
+ for the real address could surface a 550 from the fake address used
8
+ to detect accept-all servers, making it look like the verifier had
9
+ ignored a hard bounce. The catch-all probe now runs silently and
10
+ only the real address's SMTP response is reported.
11
+
12
+ ## 0.2.0
13
+
14
+ - Add `smtp_response` to the result: the SMTP code and message returned by
15
+ the destination MX (e.g. `{ code: "421", message: "Greylisted, try again" }`).
16
+ `nil` when no SMTP probe was made or no response was received.
17
+ - Add `retryable` boolean to the result. `true` when the verifier hit a
18
+ transient signal (4xx response, server-busy, timeout, connection refused, or
19
+ a transient DNS error) and the result is `risky`. Callers can use this to
20
+ decide whether to enqueue a follow-up verification.
21
+
3
22
  ## 0.1.0
4
23
 
5
24
  - 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],
@@ -217,7 +222,7 @@ module Deliverable
217
222
  problematic_server?(mail_server) ? :assumed : (last || :assumed)
218
223
  end
219
224
 
220
- def attempt_rcpt(mail_server, port, recipient)
225
+ def attempt_rcpt(mail_server, port, recipient, record: true)
221
226
  Timeout.timeout(@timeout) do
222
227
  smtp = Net::SMTP.new(mail_server, port)
223
228
  smtp.enable_starttls_auto if port == 587 || port == 465
@@ -225,44 +230,80 @@ 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 record && response.respond_to?(:status)
229
235
  return :verified
230
236
  rescue Net::SMTPFatalError => e
231
- classify_fatal(e.message)
232
- rescue Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPAuthenticationError
237
+ record_error(e) if record
238
+ classify_fatal(e.message, record: record)
239
+ rescue Net::SMTPServerBusy => e
240
+ record_error(e, retryable: true) if record
241
+ return :assumed
242
+ rescue Net::SMTPSyntaxError, Net::SMTPAuthenticationError => e
243
+ record_error(e) if record
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) if record
250
+ :connection_failed
251
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH => e
252
+ record_transient(e.message) if record
238
253
  :connection_failed
239
- rescue Net::SMTPAuthenticationError, Net::SMTPServerBusy
254
+ rescue Net::SMTPAuthenticationError => e
255
+ record_error(e) if record
256
+ :assumed
257
+ rescue Net::SMTPServerBusy => e
258
+ record_error(e, retryable: true) if record
240
259
  :assumed
241
260
  rescue StandardError => e
242
261
  log "SMTP probe failed for #{recipient} on #{mail_server}:#{port}: #{e.message}"
243
262
  :connection_failed
244
263
  end
245
264
 
246
- 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")
265
+ def classify_fatal(message, record: true)
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 if record
253
272
  :assumed
254
273
  else
274
+ if record && 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
262
303
 
263
304
  fake = "definitely-does-not-exist-#{rand(10**10)}@#{domain}"
264
305
  result = SMTP_PORTS.each do |port|
265
- outcome = attempt_rcpt(mail_server, port, fake)
306
+ outcome = attempt_rcpt(mail_server, port, fake, record: false)
266
307
  break :accepted if outcome == :verified
267
308
  break :rejected if outcome == :invalid
268
309
  end
@@ -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.1"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Strömberg
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: valid_email2