millionsend 0.2.0 → 0.3.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: e15bda7d83e77437273c93cad192bc1da266e48ffb560b29037c898e26cc825d
4
- data.tar.gz: be0dbc70f2210ff128541664405e74278f03825a2ea3e5eaa7ef887ce8c73a3b
3
+ metadata.gz: 3a05f5a8a6f73d20210b65a538c84b985206374628dd5298793c01d37bd9138f
4
+ data.tar.gz: 7fe8ff20393c98a38072d70617e0df869aefebdb14a577db320f8514d17eca05
5
5
  SHA512:
6
- metadata.gz: 2a851400dfdff77ecd956e54df92b6ce0bf8b201c471b5354734e43a27dae34d66dff76602d61ac347758da0ee027f3cd9d331653752097dd13d637f10a38a17
7
- data.tar.gz: aded65d56245645bef51bf1aa9954caf9f212c7c73e88946d3e6bc39ec59849ef3c05fd522b5dc259548c2408b01c834b684cd3609874ff23df4c960837d7e6c
6
+ metadata.gz: ec4a59041ec45a8c620b9fa2ff618971273b0dc6ba43ba75e758cec70653978e9a428249acd52c060b79ab83cdc70b5f119b019b2407b2ce802f2407444cbee9
7
+ data.tar.gz: 2fc47253ed0511c373c8273f84457ad90f812db1d4d6d9df8c4d58019780e34e484681239679033f9b9d0ca435cebac24c19f23f5438c26cd92c2d388bf6be85
data/README.md CHANGED
@@ -48,10 +48,15 @@ on any non-2xx response (see [Error handling](#error-handling)).
48
48
  Millionsend.api_key = "ms_123" # falls back to ENV["MILLIONSEND_API_KEY"]
49
49
  Millionsend.base_url = "https://mail.acme.dev" # falls back to ENV["MILLIONSEND_BASE_URL"],
50
50
  # then http://localhost:3001
51
+ Millionsend.allow_insecure_http = false # accept a non-loopback http:// base_url
51
52
  ```
52
53
 
53
54
  MillionSend is self-hosted, so there is no cloud default — **set `base_url` to your
54
55
  deployment in production.** An explicitly assigned value always wins over the environment.
56
+ Plain `http://` is only accepted for loopback hosts (`localhost`, `127.0.0.1`, `::1`); any
57
+ other `http://` URL raises `Millionsend::ApplicationError` on the first call, since the API
58
+ key is sent as a bearer header. Set `allow_insecure_http = true` to talk to a non-TLS
59
+ instance elsewhere (e.g. inside a private network).
55
60
  Params are symbol-keyed hashes and map straight to the wire (Ruby's snake_case is already
56
61
  the wire's snake_case: `reply_to`, `scheduled_at`, `segment_id`).
57
62
 
@@ -61,7 +66,8 @@ the wire's snake_case: `reply_to`, `scheduled_at`, `segment_id`).
61
66
 
62
67
  ```ruby
63
68
  Millionsend::Emails.send(payload, idempotency_key: "order-42") # POST /emails
64
- Millionsend::Emails.get(id) # GET /emails/:id
69
+ Millionsend::Emails.get(id) # GET /emails/:id (includes score: 0-10 or nil)
70
+ Millionsend::Emails.get_insights(id) # GET /emails/:id/insights (404 until computed)
65
71
  Millionsend::Emails.cancel(id) # POST /emails/:id/cancel (scheduled only)
66
72
 
67
73
  Millionsend::Batch.send([payload_a, payload_b], idempotency_key: "run-7") # up to 100
@@ -132,6 +138,19 @@ Millionsend::Segments.update(segment[:id], name: "Pro tier")
132
138
  Millionsend::Segments.remove(segment[:id])
133
139
  ```
134
140
 
141
+ ### Deliverability (MillionSend extension)
142
+
143
+ Per-email best-practice insights and the account-level deliverability score.
144
+
145
+ ```ruby
146
+ insights = Millionsend::Emails.get_insights(email[:id]) # score, band, checks: [{id:, severity:, status:, penalty:, detail:}]
147
+ account = Millionsend::Deliverability.get # GET /deliverability — trailing-30-day account score
148
+ puts account[:score] # 0-10 (one decimal) or nil when there is not enough data
149
+ ```
150
+
151
+ Check ids and the band/severity/status values are an open set that grows across score
152
+ versions — treat them as strings, not a closed enum.
153
+
135
154
  ## Error handling
136
155
 
137
156
  No `{ data, error }` tuple — a non-2xx response raises. The base class is `Millionsend::Error`,
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Millionsend
4
+ # Account-level deliverability score over the trailing window
5
+ # (scores are 0-10, one decimal; null means not enough data).
6
+ module Deliverability
7
+ class << self
8
+ # GET /deliverability
9
+ def get
10
+ Millionsend::Request.new(method: :get, path: "/deliverability").perform
11
+ end
12
+ end
13
+ end
14
+ end
@@ -20,6 +20,14 @@ module Millionsend
20
20
  Millionsend::Request.new(method: :get, path: "/emails/#{Millionsend::Util.encode(id)}").perform
21
21
  end
22
22
 
23
+ # GET /emails/:id/insights — the pre-send best-practice report computed
24
+ # when the email was sent. 404 when the email is unknown or has no
25
+ # insights yet. Check ids and band/severity/status values are an open
26
+ # set that grows across score versions; they arrive as plain strings.
27
+ def get_insights(id)
28
+ Millionsend::Request.new(method: :get, path: "/emails/#{Millionsend::Util.encode(id)}/insights").perform
29
+ end
30
+
23
31
  # POST /emails/:id/cancel — scheduled, unsent emails only.
24
32
  def cancel(id)
25
33
  Millionsend::Request.new(method: :post, path: "/emails/#{Millionsend::Util.encode(id)}/cancel").perform
@@ -65,6 +65,13 @@ module Millionsend
65
65
  def build_uri
66
66
  base = Millionsend.base_url.to_s.sub(%r{/+\z}, "")
67
67
  uri = URI.parse("#{base}#{@path}")
68
+ if !Millionsend.allow_insecure_http && Millionsend::Util.insecure_http?(uri)
69
+ raise Millionsend::ApplicationError.new(
70
+ "Refusing to send the API key over plain http to #{base}. " \
71
+ "Use https, or set Millionsend.allow_insecure_http = true.",
72
+ nil, "application_error"
73
+ )
74
+ end
68
75
  query = (@query || {}).reject { |_, v| v.nil? }
69
76
  uri.query = URI.encode_www_form(query) unless query.empty?
70
77
  uri
@@ -16,6 +16,16 @@ module Millionsend
16
16
  URI.encode_www_form_component(value.to_s).gsub("+", "%20")
17
17
  end
18
18
 
19
+ LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
20
+
21
+ # True for an http:// URI whose host is not loopback.
22
+ def insecure_http?(uri)
23
+ return false unless uri.scheme == "http"
24
+
25
+ host = uri.host.to_s.downcase
26
+ !LOOPBACK_HOSTS.include?(host) && !host.start_with?("127.")
27
+ end
28
+
19
29
  # The keyset pagination params every list endpoint accepts. nil values are
20
30
  # dropped when the query string is built.
21
31
  def list_query(options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Millionsend
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/millionsend.rb CHANGED
@@ -10,6 +10,7 @@ require "millionsend/contacts"
10
10
  require "millionsend/topics"
11
11
  require "millionsend/broadcasts"
12
12
  require "millionsend/segments"
13
+ require "millionsend/deliverability"
13
14
 
14
15
  # Ruby client for the MillionSend HTTP API. Configure once, then call the
15
16
  # resource modules:
@@ -21,14 +22,17 @@ require "millionsend/segments"
21
22
  #
22
23
  # api_key falls back to the MILLIONSEND_API_KEY env var; base_url to
23
24
  # MILLIONSEND_BASE_URL and then http://localhost:3001 (MillionSend is
24
- # self-hosted, so there is no cloud default). Every call returns a symbol-keyed
25
- # Hash on success and raises a Millionsend::Error on any non-2xx response.
25
+ # self-hosted, so there is no cloud default). Plain http is only accepted for
26
+ # loopback hosts unless allow_insecure_http is set, since the API key travels
27
+ # as a bearer header. Every call returns a symbol-keyed Hash on success and
28
+ # raises a Millionsend::Error on any non-2xx response.
26
29
  module Millionsend
27
30
  DEFAULT_BASE_URL = "http://localhost:3001"
28
31
  USER_AGENT = "millionsend-ruby/#{VERSION}"
29
32
 
30
33
  class << self
31
34
  attr_writer :api_key, :base_url
35
+ attr_accessor :allow_insecure_http
32
36
 
33
37
  def api_key
34
38
  @api_key || ENV["MILLIONSEND_API_KEY"]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: millionsend
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MillionSend
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-17 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -66,6 +66,7 @@ files:
66
66
  - lib/millionsend/batch.rb
67
67
  - lib/millionsend/broadcasts.rb
68
68
  - lib/millionsend/contacts.rb
69
+ - lib/millionsend/deliverability.rb
69
70
  - lib/millionsend/emails.rb
70
71
  - lib/millionsend/error.rb
71
72
  - lib/millionsend/request.rb