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 +4 -4
- data/README.md +18 -21
- data/lib/millionsend/broadcasts.rb +2 -1
- data/lib/millionsend/contacts.rb +18 -26
- data/lib/millionsend/segments.rb +12 -12
- data/lib/millionsend/version.rb +1 -1
- data/lib/millionsend.rb +0 -1
- metadata +5 -7
- data/lib/millionsend/audiences.rb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e15bda7d83e77437273c93cad192bc1da266e48ffb560b29037c898e26cc825d
|
|
4
|
+
data.tar.gz: be0dbc70f2210ff128541664405e74278f03825a2ea3e5eaa7ef887ce8c73a3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`, `
|
|
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
|
-
###
|
|
73
|
+
### Contacts
|
|
74
|
+
|
|
75
|
+
Contacts are team-global — one list per team, no audiences.
|
|
74
76
|
|
|
75
77
|
```ruby
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Millionsend::
|
|
79
|
-
Millionsend::
|
|
80
|
-
|
|
81
|
-
Millionsend::Contacts.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
no Resend equivalent
|
|
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
|
-
-
|
|
178
|
-
|
|
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
|
|
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
|
data/lib/millionsend/contacts.rb
CHANGED
|
@@ -1,59 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Millionsend
|
|
4
|
-
# Contacts
|
|
5
|
-
#
|
|
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 /
|
|
9
|
-
# addressing, not a body field, so it is stripped from the payload.
|
|
8
|
+
# POST /contacts
|
|
10
9
|
def create(params)
|
|
11
|
-
|
|
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
|
|
17
|
-
Millionsend::Request.new(method: :get, path: member_path(id_or_email
|
|
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 (:
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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, _| [:
|
|
26
|
-
Millionsend::Request.new(method: :patch, path: member_path(key
|
|
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
|
|
31
|
-
Millionsend::Request.new(method: :delete, path: member_path(id_or_email
|
|
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
|
|
32
|
+
# GET /contacts — accepts limit:/after:/before:.
|
|
35
33
|
def list(options = {})
|
|
36
34
|
Millionsend::Request.new(
|
|
37
|
-
method: :get, path:
|
|
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
|
|
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
|
|
51
|
-
|
|
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
|
data/lib/millionsend/segments.rb
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Millionsend
|
|
4
|
-
# Dynamic segments — a saved filter over
|
|
5
|
-
# extension with no Resend equivalent
|
|
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 /
|
|
8
|
+
# POST /segments
|
|
9
9
|
def create(params)
|
|
10
|
-
Millionsend::Request.new(method: :post, path: "/
|
|
10
|
+
Millionsend::Request.new(method: :post, path: "/segments", body: params).perform
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# GET /
|
|
13
|
+
# GET /segments/:id — also returns a live contact_count.
|
|
14
14
|
def get(id)
|
|
15
|
-
Millionsend::Request.new(method: :get, path: "/
|
|
15
|
+
Millionsend::Request.new(method: :get, path: "/segments/#{Millionsend::Util.encode(id)}").perform
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# GET /
|
|
18
|
+
# GET /segments — accepts limit:/after:/before:.
|
|
19
19
|
def list(options = {})
|
|
20
20
|
Millionsend::Request.new(
|
|
21
|
-
method: :get, path: "/
|
|
21
|
+
method: :get, path: "/segments", query: Millionsend::Util.list_query(options)
|
|
22
22
|
).perform
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# PATCH /
|
|
25
|
+
# PATCH /segments/:id
|
|
26
26
|
def update(id, params)
|
|
27
|
-
Millionsend::Request.new(method: :patch, path: "/
|
|
27
|
+
Millionsend::Request.new(method: :patch, path: "/segments/#{Millionsend::Util.encode(id)}", body: params).perform
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# DELETE /
|
|
30
|
+
# DELETE /segments/:id
|
|
31
31
|
def remove(id)
|
|
32
|
-
Millionsend::Request.new(method: :delete, path: "/
|
|
32
|
+
Millionsend::Request.new(method: :delete, path: "/segments/#{Millionsend::Util.encode(id)}").perform
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
data/lib/millionsend/version.rb
CHANGED
data/lib/millionsend.rb
CHANGED
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.
|
|
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-
|
|
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,
|
|
56
|
-
|
|
57
|
-
|
|
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
|