millionsend 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: da6448e6cf8b24c1c2badb8bb4c4d4904d873882ede341fefdbcb6949561753e
4
- data.tar.gz: e68c0e3c988f18fee505aa780fbfc1974593da463f1aba01162eff0e50d4a0b4
3
+ metadata.gz: e15bda7d83e77437273c93cad192bc1da266e48ffb560b29037c898e26cc825d
4
+ data.tar.gz: be0dbc70f2210ff128541664405e74278f03825a2ea3e5eaa7ef887ce8c73a3b
5
5
  SHA512:
6
- metadata.gz: dc6a03a18b27ff92b46203c37df82eed46687420b14ac010e7b671ca6506f4f4c008e9177728c4640bb8aff3019f67fd7dbf23b9636a64a75b48cbfc658e2007
7
- data.tar.gz: f5988d2662081f5bb34323cf482c10fc3762051b0b44e307a391cb867214d6dd6306664b052ed5f59e6ea5e2ef0e9187f6dba582c1c1b9c5ca4341ecdc076c4d
6
+ metadata.gz: 2a851400dfdff77ecd956e54df92b6ce0bf8b201c471b5354734e43a27dae34d66dff76602d61ac347758da0ee027f3cd9d331653752097dd13d637f10a38a17
7
+ data.tar.gz: aded65d56245645bef51bf1aa9954caf9f212c7c73e88946d3e6bc39ec59849ef3c05fd522b5dc259548c2408b01c834b684cd3609874ff23df4c960837d7e6c
data/README.md CHANGED
@@ -53,7 +53,7 @@ Millionsend.base_url = "https://mail.acme.dev" # falls back to ENV["MILLIONSEND_
53
53
  MillionSend is self-hosted, so there is no cloud default — **set `base_url` to your
54
54
  deployment in production.** An explicitly assigned value always wins over the environment.
55
55
  Params are symbol-keyed hashes and map straight to the wire (Ruby's snake_case is already
56
- the wire's snake_case: `reply_to`, `scheduled_at`, `audience_id`).
56
+ the wire's snake_case: `reply_to`, `scheduled_at`, `segment_id`).
57
57
 
58
58
  ## Resources
59
59
 
@@ -70,28 +70,25 @@ Millionsend::Batch.send([payload_a, payload_b], idempotency_key: "run-7") # up t
70
70
  `to`, `cc`, `bcc` and `reply_to` accept either a string or an array. `Emails.create` is
71
71
  an alias of `Emails.send` (as is `Batch.create`), mirroring Resend.
72
72
 
73
- ### Audiences & contacts
73
+ ### Contacts
74
+
75
+ Contacts are team-global — one list per team, no audiences.
74
76
 
75
77
  ```ruby
76
- audience = Millionsend::Audiences.create(name: "Registered users")
77
- Millionsend::Audiences.list(limit: 20, after: cursor)
78
- Millionsend::Audiences.get(id)
79
- Millionsend::Audiences.remove(id)
80
-
81
- Millionsend::Contacts.create(audience_id: audience[:id], email: "ada@acme.dev",
82
- first_name: "Ada", properties: { plan: "pro" })
83
- Millionsend::Contacts.get("ada@acme.dev", audience_id: audience[:id]) # by id or email
84
- Millionsend::Contacts.get(contact_id) # top-level, by id
85
- Millionsend::Contacts.update(id: contact_id, unsubscribed: true, first_name: nil) # nil clears
86
- Millionsend::Contacts.remove("ada@acme.dev", audience_id: audience[:id])
87
- Millionsend::Contacts.list(audience_id: audience[:id], limit: 50)
78
+ contact = Millionsend::Contacts.create(email: "ada@acme.dev", first_name: "Ada",
79
+ properties: { plan: "pro" })
80
+ Millionsend::Contacts.get("ada@acme.dev") # by id or email
81
+ Millionsend::Contacts.update(id: contact[:id], unsubscribed: true, first_name: nil) # nil clears
82
+ Millionsend::Contacts.remove("ada@acme.dev")
83
+ Millionsend::Contacts.list(limit: 50)
88
84
 
89
85
  # Topic subscriptions (granular unsubscribe) — mirrors resend's contacts.topics.update
90
86
  Millionsend::Contacts.topics_update("ada@acme.dev", [{ id: topic_id, subscription: "opt_out" }])
91
87
  ```
92
88
 
93
89
  Contacts are addressable by id or email; when an `update` hash carries both, the email wins.
94
- Omit `audience_id:` to use the top-level `/contacts` endpoints.
90
+ Emails are unique per team (case-insensitive) — a duplicate `create` raises
91
+ `Millionsend::ValidationError`.
95
92
 
96
93
  ### Topics
97
94
 
@@ -106,7 +103,7 @@ Millionsend::Topics.remove(id)
106
103
 
107
104
  ```ruby
108
105
  broadcast = Millionsend::Broadcasts.create(
109
- audience_id: audience[:id],
106
+ segment_id: segment[:id], # optional; omit segment_id and topic_id to send to all contacts
110
107
  from: "Acme <news@acme.dev>",
111
108
  subject: "Launch",
112
109
  html: "<p>Hi {{{FIRST_NAME|there}}}</p>"
@@ -121,13 +118,12 @@ Millionsend::Broadcasts.remove(broadcast[:id]) # draft only
121
118
 
122
119
  ### Segments (MillionSend extension)
123
120
 
124
- Dynamic segments are a saved filter over an audience's contacts — a MillionSend superset with
125
- no Resend equivalent, served under `/segments2`.
121
+ Dynamic segments are a saved filter over the team's contacts — a MillionSend superset with
122
+ no Resend equivalent.
126
123
 
127
124
  ```ruby
128
125
  segment = Millionsend::Segments.create(
129
126
  name: "Pro plan",
130
- audience_id: audience[:id],
131
127
  filter: { match: "all", conditions: [{ field: "property:plan", op: "equals", value: "pro" }] }
132
128
  )
133
129
  Millionsend::Segments.get(segment[:id]) # includes a live contact_count
@@ -174,8 +170,9 @@ Method names, nesting and payloads match. Notes:
174
170
  are no `Domains` / `ApiKeys` resources here.
175
171
  - Resend's `Contacts.topics.update` becomes `Millionsend::Contacts.topics_update` (Ruby has no
176
172
  nested-module method on a module function).
177
- - `Millionsend::Segments` is the distinct dynamic-filter feature (`/segments2`), not Resend's
178
- audiences alias. Use `Millionsend::Audiences` for a straight port.
173
+ - **No audiences** contacts are team-global, so there is no `Audiences` resource and no
174
+ `audience_id` params. `Millionsend::Segments` is the dynamic-filter feature (`/segments`),
175
+ not Resend's audiences alias.
179
176
 
180
177
  ## License
181
178
 
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Millionsend
4
- # Broadcasts — one email sent to a whole audience or segment.
4
+ # Broadcasts — one email sent to a segment, a topic's subscribers, or every
5
+ # contact (pass neither segment_id nor topic_id).
5
6
  module Broadcasts
6
7
  class << self
7
8
  # POST /broadcasts
@@ -1,59 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Millionsend
4
- # Contacts live inside an audience or at the top level, and are addressable by
5
- # id or by email (email wins when an update hash carries both).
4
+ # Contacts are team-global and addressable by id or by email (email wins when
5
+ # an update hash carries both).
6
6
  module Contacts
7
7
  class << self
8
- # POST /audiences/:audience_id/contacts (or /contacts). audience_id is
9
- # addressing, not a body field, so it is stripped from the payload.
8
+ # POST /contacts
10
9
  def create(params)
11
- body = params.reject { |k, _| k == :audience_id }
12
- Millionsend::Request.new(method: :post, path: collection_path(params[:audience_id]), body: body).perform
10
+ Millionsend::Request.new(method: :post, path: "/contacts", body: params).perform
13
11
  end
14
12
 
15
13
  # GET a single contact by id or email.
16
- def get(id_or_email, audience_id: nil)
17
- Millionsend::Request.new(method: :get, path: member_path(id_or_email, audience_id)).perform
14
+ def get(id_or_email)
15
+ Millionsend::Request.new(method: :get, path: member_path(id_or_email)).perform
18
16
  end
19
17
 
20
- # PATCH a contact. Addressing keys (:audience_id, :id, :email) are pulled
21
- # out; everything else is the body. A nil value clears a field; omit a key
22
- # to leave it unchanged.
18
+ # PATCH a contact. Addressing keys (:id, :email) are pulled out;
19
+ # everything else is the body. A nil value clears a field; omit a key to
20
+ # leave it unchanged.
23
21
  def update(params)
24
22
  key = params[:email] || params[:id]
25
- body = params.reject { |k, _| [:audience_id, :id, :email].include?(k) }
26
- Millionsend::Request.new(method: :patch, path: member_path(key, params[:audience_id]), body: body).perform
23
+ body = params.reject { |k, _| [:id, :email].include?(k) }
24
+ Millionsend::Request.new(method: :patch, path: member_path(key), body: body).perform
27
25
  end
28
26
 
29
27
  # DELETE a contact by id or email.
30
- def remove(id_or_email, audience_id: nil)
31
- Millionsend::Request.new(method: :delete, path: member_path(id_or_email, audience_id)).perform
28
+ def remove(id_or_email)
29
+ Millionsend::Request.new(method: :delete, path: member_path(id_or_email)).perform
32
30
  end
33
31
 
34
- # GET a list of contacts; pass audience_id: to scope it, plus limit:/after:/before:.
32
+ # GET /contacts accepts limit:/after:/before:.
35
33
  def list(options = {})
36
34
  Millionsend::Request.new(
37
- method: :get, path: collection_path(options[:audience_id]), query: Millionsend::Util.list_query(options)
35
+ method: :get, path: "/contacts", query: Millionsend::Util.list_query(options)
38
36
  ).perform
39
37
  end
40
38
 
41
39
  # PATCH /contacts/:id_or_email/topics with a bare array of
42
40
  # { id:, subscription: }. Mirrors resend-ruby's contacts.topics.update.
43
41
  def topics_update(id_or_email, topics)
44
- path = "/contacts/#{Millionsend::Util.encode(id_or_email)}/topics"
45
- Millionsend::Request.new(method: :patch, path: path, body: topics).perform
42
+ Millionsend::Request.new(method: :patch, path: "#{member_path(id_or_email)}/topics", body: topics).perform
46
43
  end
47
44
 
48
45
  private
49
46
 
50
- def collection_path(audience_id)
51
- audience_id ? "/audiences/#{Millionsend::Util.encode(audience_id)}/contacts" : "/contacts"
52
- end
53
-
54
- def member_path(id_or_email, audience_id)
55
- key = Millionsend::Util.encode(id_or_email)
56
- audience_id ? "/audiences/#{Millionsend::Util.encode(audience_id)}/contacts/#{key}" : "/contacts/#{key}"
47
+ def member_path(id_or_email)
48
+ "/contacts/#{Millionsend::Util.encode(id_or_email)}"
57
49
  end
58
50
  end
59
51
  end
@@ -1,35 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Millionsend
4
- # Dynamic segments — a saved filter over an audience's contacts. A MillionSend
5
- # extension with no Resend equivalent; served under /segments2.
4
+ # Dynamic segments — a saved filter over the team's contacts. A MillionSend
5
+ # extension with no Resend equivalent.
6
6
  module Segments
7
7
  class << self
8
- # POST /segments2
8
+ # POST /segments
9
9
  def create(params)
10
- Millionsend::Request.new(method: :post, path: "/segments2", body: params).perform
10
+ Millionsend::Request.new(method: :post, path: "/segments", body: params).perform
11
11
  end
12
12
 
13
- # GET /segments2/:id — also returns a live contact_count.
13
+ # GET /segments/:id — also returns a live contact_count.
14
14
  def get(id)
15
- Millionsend::Request.new(method: :get, path: "/segments2/#{Millionsend::Util.encode(id)}").perform
15
+ Millionsend::Request.new(method: :get, path: "/segments/#{Millionsend::Util.encode(id)}").perform
16
16
  end
17
17
 
18
- # GET /segments2 — accepts limit:/after:/before:.
18
+ # GET /segments — accepts limit:/after:/before:.
19
19
  def list(options = {})
20
20
  Millionsend::Request.new(
21
- method: :get, path: "/segments2", query: Millionsend::Util.list_query(options)
21
+ method: :get, path: "/segments", query: Millionsend::Util.list_query(options)
22
22
  ).perform
23
23
  end
24
24
 
25
- # PATCH /segments2/:id
25
+ # PATCH /segments/:id
26
26
  def update(id, params)
27
- Millionsend::Request.new(method: :patch, path: "/segments2/#{Millionsend::Util.encode(id)}", body: params).perform
27
+ Millionsend::Request.new(method: :patch, path: "/segments/#{Millionsend::Util.encode(id)}", body: params).perform
28
28
  end
29
29
 
30
- # DELETE /segments2/:id
30
+ # DELETE /segments/:id
31
31
  def remove(id)
32
- Millionsend::Request.new(method: :delete, path: "/segments2/#{Millionsend::Util.encode(id)}").perform
32
+ Millionsend::Request.new(method: :delete, path: "/segments/#{Millionsend::Util.encode(id)}").perform
33
33
  end
34
34
  end
35
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Millionsend
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/millionsend.rb CHANGED
@@ -6,7 +6,6 @@ require "millionsend/error"
6
6
  require "millionsend/request"
7
7
  require "millionsend/emails"
8
8
  require "millionsend/batch"
9
- require "millionsend/audiences"
10
9
  require "millionsend/contacts"
11
10
  require "millionsend/topics"
12
11
  require "millionsend/broadcasts"
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.1.0
4
+ version: 0.2.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-16 00:00:00.000000000 Z
11
+ date: 2026-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -52,10 +52,9 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.19'
55
- description: 'Ruby client for the MillionSend HTTP API: emails, batch, audiences,
56
- contacts, topics, broadcasts, and dynamic segments. Wire-compatible with Resend
57
- and mirror-shaped after resend-ruby, so migrating is mostly an import swap plus
58
- a base_url.'
55
+ description: 'Ruby client for the MillionSend HTTP API: emails, batch, contacts, topics,
56
+ broadcasts, and dynamic segments. Wire-compatible with Resend and mirror-shaped
57
+ after resend-ruby, so migrating is mostly an import swap plus a base_url.'
59
58
  email:
60
59
  executables: []
61
60
  extensions: []
@@ -64,7 +63,6 @@ files:
64
63
  - LICENSE
65
64
  - README.md
66
65
  - lib/millionsend.rb
67
- - lib/millionsend/audiences.rb
68
66
  - lib/millionsend/batch.rb
69
67
  - lib/millionsend/broadcasts.rb
70
68
  - lib/millionsend/contacts.rb
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Millionsend
4
- # Audiences — named contact lists (Resend-compatible).
5
- module Audiences
6
- class << self
7
- # POST /audiences
8
- def create(params)
9
- Millionsend::Request.new(method: :post, path: "/audiences", body: { name: params[:name] }).perform
10
- end
11
-
12
- # GET /audiences/:id
13
- def get(id)
14
- Millionsend::Request.new(method: :get, path: "/audiences/#{Millionsend::Util.encode(id)}").perform
15
- end
16
-
17
- # GET /audiences — accepts limit:/after:/before:.
18
- def list(options = {})
19
- Millionsend::Request.new(
20
- method: :get, path: "/audiences", query: Millionsend::Util.list_query(options)
21
- ).perform
22
- end
23
-
24
- # DELETE /audiences/:id
25
- def remove(id)
26
- Millionsend::Request.new(method: :delete, path: "/audiences/#{Millionsend::Util.encode(id)}").perform
27
- end
28
- end
29
- end
30
- end