sendclipper 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: a3f2ca79b7b603e79b2a71e81fd3bf8033e705b0c76171c8a69197db1e08cf45
4
- data.tar.gz: f97e10cb672f61fb581b866d02fb884b62485bfbc1c63a2721d3e1c116d6223b
3
+ metadata.gz: 4044288ac0d8d8dddaa8e914c9ee649cf24d31043c6135360788ea7fccb7522f
4
+ data.tar.gz: f3cf0b37bb3c1e641e7ec4121c580a186c39003dadd2e308d540008a7d8bb89c
5
5
  SHA512:
6
- metadata.gz: cef70da38935cb7581232e23fe51041f5b0a9f2d3cb845b956dfdb3608762b4165c78b7fd17af857f99ba2301102167a841139e37c0cdb00b3337f3ebe21d95e
7
- data.tar.gz: fb85cd253f346b37a46907366bf8cfa8ce8459b3f731ce1d4d7089ff17bf0839094f2d1826950cf1f644e54c0d7f128140fbe0d1b465acd1bd1bf9ef60faed8e
6
+ metadata.gz: 822f264d7447e5d43e2280a65b04792f8c317507b26a30f77dc4892995c6f28a1cacd5c1884cf7b548918616271acb4aac9b38d621eba3219c567ef70efbb867
7
+ data.tar.gz: 68f7d996faf5e10b92f91a25babcb7e60fc37d07632d5a5e4c38c2887a0c5fcbef08042528596bab1fd051f2dee871be1eecdd0e32cf2eabc41f8665ed079a0a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — 2026-10-18
4
+
5
+ - `emails.send`, `sandboxes.send` and each `batch.send` item take `reply_to:` (the API's `replyTo`): one address or a list of up to 10, sent as the `Reply-To` header.
6
+ - `emails.send` and `sandboxes.send` take `attachments:`, a list of `{ filename:, content:, content_type: }`. `content:` is the file's raw bytes (`File.binread("invoice.pdf")`) and the SDK base64-encodes it; pass `content_base64:` instead when you already hold base64, and it is sent as it is. Giving both, or a `content:` that is not a String, raises `ArgumentError` before any request. Limits: 10 MiB per file, 25 MiB per message, 20 files. A batch item with attachments is refused by the API (`attachments_not_batchable`).
7
+ - Needs the SendClipper API from 2026-10-18 on; an older API ignores both fields.
8
+
3
9
  ## 0.1.0 — 2026-10-16
4
10
 
5
11
  First release.
data/README.md CHANGED
@@ -43,6 +43,23 @@ Fields are keyword arguments (or a Hash). Top-level names may be snake_case (`te
43
43
 
44
44
  `from` must be a bare address on a domain you verified in SendClipper. Each message has one recipient in `to`; for several, use `batch.send`.
45
45
 
46
+ ## Reply-To and attachments
47
+
48
+ ```ruby
49
+ data, error = client.emails.send(
50
+ from: "bookings@acme.io",
51
+ to: "ada@lumen.io",
52
+ subject: "Your booking",
53
+ html: "<p>Your confirmation is attached.</p>",
54
+ reply_to: "support@acme.io",
55
+ attachments: [
56
+ { filename: "booking-42.pdf", content: File.binread("booking-42.pdf") }
57
+ ]
58
+ )
59
+ ```
60
+
61
+ `reply_to:` is one address or a list of up to 10. `content:` is the file's raw bytes (read it with `File.binread`) and the SDK base64-encodes it; if you already hold base64, pass `content_base64:` instead and it is sent as it is. `content_type:` is guessed from the filename when you leave it out. Limits: 10 MiB per file, 25 MiB of files per message, and your sending provider's own. Attachments are not accepted on a batch item (it fails with `attachments_not_batchable`); send those emails on their own. `sandboxes.send` takes the same fields.
62
+
46
63
  ## Send with a template
47
64
 
48
65
  Design and publish a template in the dashboard, then send it by id or by its send name:
@@ -29,7 +29,43 @@ module SendClipper
29
29
  fields.each_with_object({}) do |(k, v), out|
30
30
  next if v.nil?
31
31
 
32
- out[k.to_s.gsub(/_([a-z0-9])/) { Regexp.last_match(1).upcase }] = v
32
+ out[camel(k)] = v
33
+ end
34
+ end
35
+
36
+ def camel(key) = key.to_s.gsub(/_([a-z0-9])/) { Regexp.last_match(1).upcase }
37
+
38
+ # The fields of one message (emails.send, sandboxes.send, a batch item):
39
+ # camelized, with each attachment's +content:+ (the file's raw bytes)
40
+ # base64-encoded. +content_base64:+ is content that is already base64,
41
+ # sent as it is.
42
+ def email_fields(fields)
43
+ out = camelize(fields)
44
+ list = out["attachments"]
45
+ out["attachments"] = list.map { |a| attachment_field(a) } if list.is_a?(Array)
46
+ out
47
+ end
48
+
49
+ def attachment_field(attachment)
50
+ given = attachment.to_h
51
+ names = given.reject { |_, v| v.nil? }.keys.map(&:to_s)
52
+ if names.include?("content") && (names & %w[content_base64 contentBase64]).any?
53
+ raise ArgumentError, "An attachment takes content: (the file's bytes) or content_base64:, not both"
54
+ end
55
+
56
+ given.each_with_object({}) do |(k, v), out|
57
+ next if v.nil?
58
+
59
+ case k.to_s
60
+ when "content"
61
+ unless v.is_a?(String)
62
+ raise ArgumentError, "An attachment's content: must be a String of the file's bytes, e.g. File.binread(path)"
63
+ end
64
+
65
+ out["content"] = [v].pack("m0")
66
+ when "content_base64", "contentBase64" then out["content"] = v
67
+ else out[camel(k)] = v
68
+ end
33
69
  end
34
70
  end
35
71
 
@@ -10,9 +10,14 @@ module SendClipper
10
10
  #
11
11
  # Every call carries an Idempotency-Key, so the SDK's own retries never
12
12
  # send twice. Pass +idempotency_key:+ to make YOUR retries safe too.
13
+ #
14
+ # +reply_to:+ is one address or a list of up to 10. +attachments:+ is a
15
+ # list of { filename:, content:, content_type: }, where +content:+ is the
16
+ # file's raw bytes (File.binread("invoice.pdf")) and the SDK base64-encodes
17
+ # it; pass +content_base64:+ instead when you already hold base64.
13
18
  def send(params = nil, **kw)
14
19
  body, options = split(params, kw)
15
- api_post("/emails", body, options: options)
20
+ api_post("/emails", email_fields(body), options: options)
16
21
  end
17
22
 
18
23
  # Same as client.batch.send.
@@ -50,11 +55,14 @@ module SendClipper
50
55
  class Batch < Base
51
56
  # Up to 500 messages in one request (one recipient each). Every item is
52
57
  # accepted or refused on its own; a refused item costs its siblings nothing.
58
+ # An item takes +reply_to:+; an item with +attachments:+ is refused
59
+ # (+attachments_not_batchable+), so send those messages on their own.
53
60
  def send(emails, **options) = self.class.post(@http, emails, options)
54
61
 
55
62
  # @api private
56
63
  def self.post(http, emails, options)
57
- items = emails.map { |e| new(http).__send__(:camelize, e.to_h) }
64
+ fields = new(http)
65
+ items = emails.map { |e| fields.__send__(:email_fields, e.to_h) }
58
66
  http.call("POST", "/emails/batch", body: { "emails" => items }, options: options)
59
67
  end
60
68
  end
@@ -38,10 +38,11 @@ module SendClipper
38
38
  def reset_credentials(id, **options) = api_post("/sandboxes/#{seg(id)}/credentials", nil, options: options)
39
39
 
40
40
  # Send to a sandbox through its JSON door, with the sandbox's own
41
- # +sc_sbx_...+ token. Same fields as emails.send; nothing is delivered.
41
+ # +sc_sbx_...+ token. Same fields as emails.send (reply_to and attachments
42
+ # included); nothing is delivered.
42
43
  def send(sandbox_token, params = nil, **kw)
43
44
  body, options = split(params, kw)
44
- api_post("/sandbox/emails", body, options: options, bearer: sandbox_token)
45
+ api_post("/sandbox/emails", email_fields(body), options: options, bearer: sandbox_token)
45
46
  end
46
47
  end
47
48
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SendClipper
4
4
  # Kept equal to the release tag by CI.
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sendclipper
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
  - SendClipper