anypost 1.1.0 → 1.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: 626219a093ae533c0f15851744e4a6eb92a42e29f793c70ac6b219c64f74f5c2
4
- data.tar.gz: 840398e4b95ff4e1bbdb6c1d5b719865304c9f0625f7e6c8f5de5b19cafbf45c
3
+ metadata.gz: 143a52ee24a58206c7af144a0ca378aa1741e8a77faa2524ba4552aa565824af
4
+ data.tar.gz: 3756ec2603f282ff45c0004cccf53496829f3cba612c36ad05de01a0ff706123
5
5
  SHA512:
6
- metadata.gz: caab7720e980e41910338662984bc706b2d731b01d3ebf4938f3db65c534b2b6f3ddd67efb014439030d71e4092763a05557477f3a6dffd0a0b615652e2219d7
7
- data.tar.gz: db2311ea2c576f98d120fb5e5f07c6914b3e4ee0c92a3fe64b94a7e294418a19b95529576070531f7ffb03a7f2393982a1581014ea0bcce34f6120c97da3b84f
6
+ metadata.gz: 595ab92c756ca037038ce60ca5b89eba8cbea436776c459b20853a2ea6f5a75940c559aeb80b7704ba0ba437ed78473bb86af5e42c67549b1735351ccef759ff
7
+ data.tar.gz: 18a5f08424ac853a62e4dfd0c77039a75521d0b968b992bc27e3dd81397f212bf3c7ddcea9ab9357bf690971ca1d330b2b9358ac29c27174cce438a1e25424ad
data/README.md CHANGED
@@ -62,7 +62,7 @@ client.email.send(
62
62
  )
63
63
  ```
64
64
 
65
- Attachment `content` is the raw file bytes: pass what `File.binread` returns and the SDK base64-encodes it. Do not pre-encode it. The request body is capped at 5 MB.
65
+ Attachment `content` is the raw file bytes: pass what `File.binread` returns and the SDK base64-encodes it. When the content is already base64 at rest, pass it as `content_base64` instead — that string is sent verbatim, so nothing is decoded just to be re-encoded. Set exactly one of the two. The request body is capped at 5 MB.
66
66
 
67
67
  ```ruby
68
68
  client.email.send(
@@ -174,8 +174,15 @@ A suppression blocks sends to an address, scoped to a `topic`. The wildcard `*`
174
174
  client.suppressions.create(email: "alice@example.com", topic: "marketing", note: "Customer requested removal")
175
175
 
176
176
  client.suppressions.delete("alice@example.com", "marketing")
177
+
178
+ client.suppressions.create_batch([
179
+ {email: "bob@example.com"},
180
+ {email: "carol@example.com", topic: "marketing", reason: "unsubscribed"}
181
+ ])
177
182
  ```
178
183
 
184
+ `create_batch` takes up to 1,000 items per call and returns `imported`, `already_suppressed`, and `invalid`. Existing entries are left unchanged, so retrying a batch is safe, and invalid addresses are reported in `invalid` instead of failing the call. A batch that would pass your team's suppression limit raises a validation error and adds nothing. Suppressions do not expire.
185
+
179
186
  `get`, `list` (with `email_contains`, `topic`, `reason`, and `origin` filters), `list_for_email`, and `delete_for_email` round out the resource. See [Suppressions](https://anypost.com/docs/reference/suppressions) for scoping and the automatic-suppression rules for bounces and complaints.
180
187
 
181
188
  ## Webhooks
@@ -4,8 +4,12 @@ module Anypost
4
4
  module Resources
5
5
  # Operations on the `/email` endpoints.
6
6
  #
7
- # Attachment `content` is the raw file bytes (e.g. from `File.binread`); the
8
- # SDK base64-encodes it for transport. Do not pre-encode it.
7
+ # Give each attachment its content exactly one of two ways: `content` for raw
8
+ # file bytes (e.g. from `File.binread`), which the SDK base64-encodes for
9
+ # transport, or `content_base64` for content that is already base64 at rest,
10
+ # which is sent through verbatim so nothing is decoded just to be re-encoded.
11
+ # Ruby has no type-level distinction between the two — both are Strings — so
12
+ # the key you choose is the signal.
9
13
  class Email < Base
10
14
  # Send a single message.
11
15
  #
@@ -35,13 +39,30 @@ module Anypost
35
39
  return message unless attachments.is_a?(Array)
36
40
 
37
41
  message = message.dup
38
- message[:attachments] = attachments.map do |attachment|
39
- content = attachment[:content]
40
- next attachment unless content.is_a?(String)
42
+ message[:attachments] = attachments.map { |attachment| encode_attachment(attachment) }
43
+ message
44
+ end
45
+
46
+ # Collapse an attachment's two possible content keys onto the single
47
+ # base64 `content` field the API takes.
48
+ def encode_attachment(attachment)
49
+ return attachment unless attachment.is_a?(Hash)
50
+
51
+ raw = attachment[:content]
52
+ encoded = attachment[:content_base64]
41
53
 
42
- attachment.merge(content: [content].pack("m0"))
54
+ if raw && encoded
55
+ raise ArgumentError,
56
+ "attachment #{attachment[:filename].inspect} sets both :content and " \
57
+ ":content_base64; set exactly one"
43
58
  end
44
- message
59
+
60
+ return attachment.except(:content_base64).merge(content: encoded) if encoded
61
+ return attachment.merge(content: [raw].pack("m0")) if raw.is_a?(String)
62
+
63
+ raise ArgumentError,
64
+ "attachment #{attachment[:filename].inspect} has no content; set :content " \
65
+ "(raw bytes) or :content_base64"
45
66
  end
46
67
  end
47
68
  end
@@ -4,7 +4,7 @@ module Anypost
4
4
  module Resources
5
5
  # Operations on the `/suppressions` endpoints. Entries key on `(email, topic)`.
6
6
  class Suppressions < Base
7
- # List the team's suppressions, newest-first. Expired rows are filtered out.
7
+ # List the team's suppressions, newest-first.
8
8
  # Filter with `email_contains`, `topic`, `reason`, and `origin`.
9
9
  def list(params = {})
10
10
  paginate("/suppressions", {
@@ -23,6 +23,20 @@ module Anypost
23
23
  request_object(:post, "/suppressions", body: params)
24
24
  end
25
25
 
26
+ # Add up to 1,000 suppressions in one call. Pass the list of items, each a
27
+ # hash with `email` and optionally `topic` (default `*`), `reason` (a free-form
28
+ # string; common aliases like `bounce` or `opt-out` are mapped, anything else
29
+ # records `manual`), and `note`. Entries that already exist are counted in
30
+ # `already_suppressed` and left unchanged, so a batch is safe to retry.
31
+ # Invalid addresses come back in `invalid` (with their `index` and `email`)
32
+ # instead of failing the batch. Raises validation_error, adding nothing, if
33
+ # the batch would pass the team's suppression limit.
34
+ #
35
+ # @return [Response] with `imported`, `already_suppressed`, and `invalid`
36
+ def create_batch(suppressions)
37
+ request_object(:post, "/suppressions/batch", body: {suppressions: suppressions})
38
+ end
39
+
26
40
  # Retrieve the suppression for an `(email, topic)` pair. Use `*` as the
27
41
  # topic for the global row. Raises not_found if the pair isn't suppressed.
28
42
  def get(email, topic)
@@ -3,5 +3,5 @@
3
3
  module Anypost
4
4
  # The single source of truth for the gem version. Bump this, tag the commit
5
5
  # `vX.Y.Z`, and push — the release workflow builds and pushes to RubyGems.
6
- VERSION = "1.1.0"
6
+ VERSION = "1.3.0"
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anypost
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anypost
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-30 00:00:00.000000000 Z
11
+ date: 2026-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday