wakio 0.1.1 → 0.1.3

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -2
  3. data/lib/wakio/delivery_method.rb +30 -11
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b10c47e80d5a8cdb7eb94e86babdcf24674b8b0563313a4b12459cbcc2ddf74
4
- data.tar.gz: 8053fbb8ebe24f105513e04e8856ea18614b86ab25b2b34e89ad8fdadabe2462
3
+ metadata.gz: 41b4696d5b5dcea8872174fce7d104adeb163eb775555de498214e2ba1c49711
4
+ data.tar.gz: 1b7eaa7f2b2e4540b4f60a4beb979718262b54a35e64c34a52fc931aa1a748d0
5
5
  SHA512:
6
- metadata.gz: d45c860e93f1c69bb3620f11dbb68179c3142bb63a8bb418e39b1c016c14d6bdba14971e3f5f3582a054dbb514f5b03c96dd8b22eeef77248058f99e8ed8ff5f
7
- data.tar.gz: 8c6f7c5cb040922d14a6b2828775bdfe6ad103cf7c1bdf07123b2722e81bd78f281caae194a803fe0852d2b3bee3ed3fdd182124918c103a23447828f1630ddc
6
+ metadata.gz: ec7af6244e5c01a97da9b8455b23475dbf4d26a86df036e0cc97ec341020b19dc7b9b851307185f5d4d2a8052a3e53cb4db37a28f350c39acd23ea6762baae07
7
+ data.tar.gz: 99b951382c14c5ee5732411f3ce2f4fc75c3128c9283d495d48763881fae5d1593630886970c55a82f89148a519a881f2e7ddc41b8968d2e252726381ffaf97b
data/README.md CHANGED
@@ -14,7 +14,9 @@ config.action_mailer.wakio_settings = {
14
14
  }
15
15
  ```
16
16
 
17
- `open_timeout` and `read_timeout` are optional, in seconds, and default to 5 and 10. A timeout raises `Net::OpenTimeout` or `Net::ReadTimeout`, not `Wakio::DeliveryError`. A read timeout can come after Wakio accepted the send, so a retry without an idempotency key (below) may send twice.
17
+ `open_timeout` and `read_timeout` are optional, in seconds, and default to 5 and 10. A timeout raises `Net::OpenTimeout` or `Net::ReadTimeout`, and any other network failure raises Ruby's own error, not `Wakio::DeliveryError`. A read timeout can come after Wakio accepted the send, so a retry without an idempotency key (below) may send twice.
18
+
19
+ `host` must be `https`. An `http` host other than `localhost` raises `Wakio::Misconfigured` before anything is sent, so the API key never travels in cleartext.
18
20
 
19
21
  Every existing mailer keeps working unchanged. Its rendered subject, its HTML and text parts, and its `from` go to Wakio as they are, with no server-side template involved. A text-only mailer sends a text-only email.
20
22
 
@@ -24,7 +26,36 @@ The `from` domain must be one of your account's sendable sending domains, matche
24
26
 
25
27
  **A suppressed address raises.** Wakio suppresses addresses that have hard-bounced or complained, and accepts a send to one rather than refusing it. The response names each recipient's status, and `deliver!` raises `Wakio::Suppressed` (a `Wakio::DeliveryError`) when every recipient was suppressed, so a one-recipient mailer to a suppressed address fails loudly instead of reporting success. Retrying cannot help, so handle it rather than letting a job backend retry it: `rescue_from Wakio::Suppressed` in your mailer also catches it under `deliver_later`. A send where only some recipients were suppressed returns normally; check the returned response's `deliveries` for which ones.
26
28
 
27
- **A refusal a retry cannot fix raises `Wakio::Rejected`.** Every 4xx except `409 request_in_flight` raises it: a revoked key, an unverified `from` domain, an idempotency key reused with a different body. It is a `Wakio::DeliveryError` carrying `status`, `code` (the response's `error`, such as `"no_verified_sending_domain"`) and the parsed `body`, so a host app can `discard_on` it or alert instead of letting a job backend retry it for weeks. A 409 and every 5xx stay a plain `Wakio::DeliveryError`, because a retry is the right response to both.
29
+ **Refusals split by whether a retry can ever send the message.** A refusal from Wakio is a `Wakio::DeliveryError` carrying `status`, `code` (the response's `error`, such as `"invalid_request"`) and the parsed `body`.
30
+
31
+ - `Wakio::Rejected`: retrying this message cannot help. `400`, `413`, `422 invalid_request` (usually a mailer bug; `detail` names the field) and `422 idempotency_key_reused` (an earlier request with that key already went out). Report it and drop it.
32
+ - `Wakio::Misconfigured`: the account or its configuration is wrong, and the same message sends once that is fixed. `401 invalid_api_key`, `402 not_entitled`, `403 account_suspended`, `404`, `422 no_verified_sending_domain`, `422 unknown_template`, a redirect, an `http` host, and any 4xx this gem does not know. Retry it.
33
+ - A plain `Wakio::DeliveryError`: `409 request_in_flight`, every 5xx, and a success response the gem cannot read. Retry it; after an unreadable success only a keyed send is safe to repeat.
34
+
35
+ Action Mailer's delivery job retries nothing itself, and Solid Queue and GoodJob do not retry a failed job by default, so give it a policy:
36
+
37
+ ```ruby
38
+ class WakioDeliveryJob < ActionMailer::MailDeliveryJob
39
+ retry_on Wakio::DeliveryError, Net::OpenTimeout, Net::ReadTimeout, wait: :polynomially_longer, attempts: 25
40
+ end
41
+
42
+ # config/application.rb
43
+ config.action_mailer.delivery_job = "WakioDeliveryJob"
44
+ ```
45
+
46
+ The mailer's `rescue_from` below catches `Suppressed` and `Rejected` before the job sees them.
47
+
48
+ ```ruby
49
+ class ApplicationMailer < ActionMailer::Base
50
+ rescue_from Wakio::Suppressed do |error|
51
+ Rails.logger.info(error.message)
52
+ end
53
+
54
+ rescue_from Wakio::Rejected do |error|
55
+ Rails.error.report(error)
56
+ end
57
+ end
58
+ ```
28
59
 
29
60
  **Fifty recipients per request.** `to`, `cc` and `bcc` are counted together, once
30
61
  case-insensitive duplicates collapse, against Wakio's own contract limit — and the
@@ -3,13 +3,10 @@ require "net/http"
3
3
  require "uri"
4
4
 
5
5
  module Wakio
6
- DeliveryError = Class.new(StandardError)
7
- Suppressed = Class.new(DeliveryError)
8
-
9
- class Rejected < DeliveryError
6
+ class DeliveryError < StandardError
10
7
  attr_reader :status, :code, :body
11
8
 
12
- def initialize(message, status:, body:)
9
+ def initialize(message = nil, status: nil, body: nil)
13
10
  super(message)
14
11
  @status = status
15
12
  @body = body
@@ -17,6 +14,10 @@ module Wakio
17
14
  end
18
15
  end
19
16
 
17
+ Suppressed = Class.new(DeliveryError)
18
+ Rejected = Class.new(DeliveryError)
19
+ Misconfigured = Class.new(DeliveryError)
20
+
20
21
  # An ActionMailer delivery method, so a host app keeps every existing mailer
21
22
  # untouched and changes only configuration.
22
23
  class DeliveryMethod
@@ -31,6 +32,14 @@ module Wakio
31
32
  # obviously ours if it ever leaks into a message dump.
32
33
  IDEMPOTENCY_HEADER = "X-Wakio-Idempotency-Key".freeze
33
34
 
35
+ # Refusals about this message itself, which no retry can fix. Every other 4xx is about the
36
+ # account or its configuration and sends once an operator fixes it, so an unknown code fails
37
+ # towards retrying rather than towards dropping mail.
38
+ REJECTED_STATUSES = [ 400, 413 ].freeze
39
+ REJECTED_CODES = %w[invalid_request idempotency_key_reused].freeze
40
+ LOCAL_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
41
+ MESSAGE_BODY_LIMIT = 300
42
+
34
43
  def deliver!(mail)
35
44
  # Caller-supplied, never derived — and no key at all when the caller gives none.
36
45
  #
@@ -47,12 +56,7 @@ module Wakio
47
56
  # deliberate repeat", because the difference is intent rather than data.
48
57
  response = post(payload_for(mail), idempotency_key: mail[IDEMPOTENCY_HEADER]&.to_s)
49
58
  status = response.code.to_i
50
- unless status.between?(200, 299)
51
- message = "Wakio returned #{status}: #{response.body}"
52
- raise Rejected.new(message, status: status, body: parsed(response.body)) if status.between?(400, 499) && status != 409
53
-
54
- raise DeliveryError, message
55
- end
59
+ raise refusal(status, response.body) unless status.between?(200, 299)
56
60
 
57
61
  # A separate check from the status above, deliberately: `accepted_any?` only ever
58
62
  # returns true or raises today, but joining the two into one `&&` condition would
@@ -129,6 +133,18 @@ module Wakio
129
133
  unreadable!(response)
130
134
  end
131
135
 
136
+ def refusal(status, raw)
137
+ body = parsed(raw)
138
+ code = body["error"] if body.is_a?(Hash)
139
+ kind =
140
+ if status.between?(300, 399) then Misconfigured
141
+ elsif !status.between?(400, 499) || status == 409 then DeliveryError
142
+ elsif REJECTED_STATUSES.include?(status) || REJECTED_CODES.include?(code) then Rejected
143
+ else Misconfigured
144
+ end
145
+ kind.new("Wakio returned #{status}: #{raw.to_s[0, MESSAGE_BODY_LIMIT]}", status: status, body: body)
146
+ end
147
+
132
148
  def parsed(body)
133
149
  JSON.parse(body.to_s)
134
150
  rescue JSON::ParserError
@@ -141,6 +157,9 @@ module Wakio
141
157
 
142
158
  def post(payload, idempotency_key:)
143
159
  uri = URI.join(settings.fetch(:host), "/v1/emails")
160
+ unless uri.scheme == "https" || LOCAL_HOSTS.include?(uri.hostname)
161
+ raise Misconfigured, "Wakio host must be https, or the API key travels in cleartext: #{settings.fetch(:host)}"
162
+ end
144
163
  request = Net::HTTP::Post.new(uri)
145
164
  request["Authorization"] = "Bearer #{settings.fetch(:api_key)}"
146
165
  request["Content-Type"] = "application/json"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wakio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - WorkersLab LLC