Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
da19395
codegen metadata
stainless-app[bot] Feb 18, 2026
cf3474e
codegen metadata
stainless-app[bot] Feb 18, 2026
e0cbedc
chore: update mock server docs
stainless-app[bot] Feb 20, 2026
d443493
feat(api): add tenantId to send
stainless-app[bot] Feb 20, 2026
1af3ac3
fix: properly mock time in ruby ci tests
stainless-app[bot] Feb 27, 2026
52ec8d2
chore(internal): codegen related update
stainless-app[bot] Mar 3, 2026
07f4b8f
chore(ci): add build step
stainless-app[bot] Mar 3, 2026
59ed473
chore(internal): codegen related update
stainless-app[bot] Mar 6, 2026
8be7c3f
chore(test): do not count install time for mock server timeout
stainless-app[bot] Mar 6, 2026
84760ce
chore(ci): skip uploading artifacts on stainless-internal branches
stainless-app[bot] Mar 7, 2026
890ab73
chore(internal): tweak CI branches
stainless-app[bot] Mar 17, 2026
dda5d01
refactor(tests): switch from prism to steady
stainless-app[bot] Mar 20, 2026
20434aa
chore(tests): bump steady to v0.19.4
stainless-app[bot] Mar 21, 2026
b648dfd
chore(tests): bump steady to v0.19.5
stainless-app[bot] Mar 21, 2026
c32fe9f
chore(internal): update gitignore
stainless-app[bot] Mar 24, 2026
b617b4c
chore(tests): bump steady to v0.19.6
stainless-app[bot] Mar 24, 2026
ca59137
chore(ci): skip lint on metadata-only changes
stainless-app[bot] Mar 25, 2026
8b6c392
chore(tests): bump steady to v0.19.7
stainless-app[bot] Mar 25, 2026
5d61b9b
fix(internal): correct multipart form field name encoding
stainless-app[bot] Mar 27, 2026
999db01
chore(ci): support opting out of skipping builds on metadata-only com…
stainless-app[bot] Mar 28, 2026
0a0a6a2
chore(tests): bump steady to v0.20.1
stainless-app[bot] Apr 1, 2026
5cac749
chore(tests): bump steady to v0.20.2
stainless-app[bot] Apr 1, 2026
dfdf1f9
fix: variable name typo
stainless-app[bot] Apr 1, 2026
1ee22e2
fix: align path encoding with RFC 3986 section 3.3
stainless-app[bot] Apr 1, 2026
fff924a
release: 0.20.0
stainless-app[bot] Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,58 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'

jobs:
build:
timeout-minutes: 10
name: build
permissions:
contents: read
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/ark-ruby' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: |-
github.repository == 'stainless-sdks/ark-ruby' &&
(github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: false
- run: |-
bundle install

- name: Get GitHub OIDC Token
if: |-
github.repository == 'stainless-sdks/ark-ruby' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());

- name: Build and upload gem artifacts
if: |-
github.repository == 'stainless-sdks/ark-ruby' &&
!startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
PACKAGE_NAME: ark_email
run: ./scripts/utils/upload-artifact.sh
lint:
timeout-minutes: 10
name: lint
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.idea/
.ignore
.prism.log
.stdy.log
.ruby-lsp/
.yardoc/
bin/tapioca
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.19.0"
".": "0.20.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 58
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-ee4b9d190e3aaa146b08bc0ffed1c802dc353c3fdc37fc0097f2350ab3714b70.yml
openapi_spec_hash: 0dad8b2e562ba7ce879425ab92169d85
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-06c3025bf12b191c3906b28173c9b359e24481dd2839dbf3e6dd0b80c1de3fd6.yml
openapi_spec_hash: d8f8fb1f78579997b6381d64cba4e826
config_hash: b70b11b10fc614f91f1c6f028b40780f
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
# Changelog

## 0.20.0 (2026-04-01)

Full Changelog: [v0.19.0...v0.20.0](https://github.com/ArkHQ-io/ark-ruby/compare/v0.19.0...v0.20.0)

### Features

* **api:** add tenantId to send ([d443493](https://github.com/ArkHQ-io/ark-ruby/commit/d4434933efda6c1cc2cab76c52319113f67aab91))


### Bug Fixes

* align path encoding with RFC 3986 section 3.3 ([1ee22e2](https://github.com/ArkHQ-io/ark-ruby/commit/1ee22e28cf4cc13a4dfe837e1e02a03fb392f747))
* **internal:** correct multipart form field name encoding ([5d61b9b](https://github.com/ArkHQ-io/ark-ruby/commit/5d61b9b1cdd09c39eebca5102e349607afa202af))
* properly mock time in ruby ci tests ([1af3ac3](https://github.com/ArkHQ-io/ark-ruby/commit/1af3ac341b43b64d6e491c72a0a8bda9408625e1))
* variable name typo ([dfdf1f9](https://github.com/ArkHQ-io/ark-ruby/commit/dfdf1f919610cfe77257d03aaf8800e8927afd9c))


### Chores

* **ci:** add build step ([07f4b8f](https://github.com/ArkHQ-io/ark-ruby/commit/07f4b8f01d304d412aef6898dc8d003f701e2733))
* **ci:** skip lint on metadata-only changes ([ca59137](https://github.com/ArkHQ-io/ark-ruby/commit/ca5913766b6889a2ea3c0966859652a01a2956b1))
* **ci:** skip uploading artifacts on stainless-internal branches ([84760ce](https://github.com/ArkHQ-io/ark-ruby/commit/84760cec19c56830abbe97179c421be8ec8d0573))
* **ci:** support opting out of skipping builds on metadata-only commits ([999db01](https://github.com/ArkHQ-io/ark-ruby/commit/999db01d79aa3e790cd164119e32a33c1161dd76))
* **internal:** codegen related update ([59ed473](https://github.com/ArkHQ-io/ark-ruby/commit/59ed473b3a2c74b6baecde451956fb1a24829489))
* **internal:** codegen related update ([52ec8d2](https://github.com/ArkHQ-io/ark-ruby/commit/52ec8d221046d735dd605aab684f17e074478cf9))
* **internal:** tweak CI branches ([890ab73](https://github.com/ArkHQ-io/ark-ruby/commit/890ab7325739769a805f7d783b06f6aed714d5cd))
* **internal:** update gitignore ([c32fe9f](https://github.com/ArkHQ-io/ark-ruby/commit/c32fe9f42f4108c3a02dd282375c2355d72c14b9))
* **test:** do not count install time for mock server timeout ([8be7c3f](https://github.com/ArkHQ-io/ark-ruby/commit/8be7c3f8333306f7e3687ade2d770a928abbf270))
* **tests:** bump steady to v0.19.4 ([20434aa](https://github.com/ArkHQ-io/ark-ruby/commit/20434aa984868e0c14f7a0aa7532ef1d13720ff0))
* **tests:** bump steady to v0.19.5 ([b648dfd](https://github.com/ArkHQ-io/ark-ruby/commit/b648dfdd94ef618155eb49efcb6356877f045e7b))
* **tests:** bump steady to v0.19.6 ([b617b4c](https://github.com/ArkHQ-io/ark-ruby/commit/b617b4c3f5bd7ecdcef434578bac5bc9e6bcedfa))
* **tests:** bump steady to v0.19.7 ([8b6c392](https://github.com/ArkHQ-io/ark-ruby/commit/8b6c3929e2e186d2549fa35f8313fe07c4b800b1))
* **tests:** bump steady to v0.20.1 ([0a0a6a2](https://github.com/ArkHQ-io/ark-ruby/commit/0a0a6a23689e7908b5d9329be7de142cca56d736))
* **tests:** bump steady to v0.20.2 ([5cac749](https://github.com/ArkHQ-io/ark-ruby/commit/5cac749910a6b44d5e822fbea298c554399065b5))
* update mock server docs ([e0cbedc](https://github.com/ArkHQ-io/ark-ruby/commit/e0cbedce73ed0f6dbd683cb3dc04b8aa9937a434))


### Refactors

* **tests:** switch from prism to steady ([dda5d01](https://github.com/ArkHQ-io/ark-ruby/commit/dda5d0168b42d6ca35dd0acb5a209d4be56e6e71))

## 0.19.0 (2026-02-07)

Full Changelog: [v0.18.0...v0.19.0](https://github.com/ArkHQ-io/ark-ruby/compare/v0.18.0...v0.19.0)
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ $ bundle exec rake

## Running tests

Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.

```bash
$ npx prism mock path/to/your/openapi.yml
```sh
$ ./scripts/mock
```

```bash
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GIT
PATH
remote: .
specs:
ark-email (0.19.0)
ark-email (0.20.0)
cgi
connection_pool

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ To use this gem, install via Bundler by adding the following to your application
<!-- x-release-please-start-version -->

```ruby
gem "ark-email", "~> 0.19.0"
gem "ark-email", "~> 0.20.0"
```

<!-- x-release-please-end -->
Expand Down
79 changes: 79 additions & 0 deletions lib/ark_email/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,97 @@ class Client < ArkEmail::Internal::Transport::BaseClient
# @return [String]
attr_reader :api_key

# Send and manage email messages.
#
# **Quick Reference:**
#
# - `POST /emails` - Send a single email
# - `POST /emails/batch` - Send up to 100 emails
# - `GET /emails/{emailId}` - Get email status and details
# - `GET /emails` - List sent emails
# - `POST /emails/{emailId}/retry` - Retry failed delivery
# @return [ArkEmail::Resources::Emails]
attr_reader :emails

# Access API request logs for debugging and monitoring.
#
# Every API request is logged with details including:
#
# - Request method, path, and endpoint
# - Response status code and duration
# - Error details (code, message) for failed requests
# - SDK information (name, version)
# - Rate limit state at time of request
# - Request and response bodies (for single log retrieval)
#
# **Retention:** Logs are retained for 90 days.
#
# **Body storage:** Request and response bodies are stored encrypted and truncated
# at 25KB. Bodies are only returned when retrieving a single log entry.
#
# **Quick Reference:**
#
# - `GET /logs` - List API request logs with filters
# - `GET /logs/{requestId}` - Get full details including request/response bodies
# @return [ArkEmail::Resources::Logs]
attr_reader :logs

# Per-tenant usage analytics and bulk reporting.
#
# Track email sending statistics for each tenant to power billing, dashboards, and
# monitoring.
#
# **Single Tenant Usage:**
#
# - `GET /tenants/{id}/usage` - Get usage stats for a specific tenant
# - `GET /tenants/{id}/usage/timeseries` - Get time-bucketed data for charts
#
# **Bulk Usage:**
#
# - `GET /usage/tenants` - Get usage for all tenants (paginated, sortable)
# - `GET /usage/export` - Export usage data as CSV, JSONL, or JSON
#
# **Period Formats:**
#
# - Shortcuts: `today`, `yesterday`, `this_month`, `last_month`, `last_7_days`,
# `last_30_days`
# - Month: `2024-01`
# - Date range: `2024-01-01..2024-01-15`
# @return [ArkEmail::Resources::Usage]
attr_reader :usage

# Check account rate limits and send limits.
#
# The limits endpoint returns current status for operational limits:
#
# - **Rate limit:** API requests per second (currently 10/sec)
# - **Send limit:** Emails per hour (default 100/hour for new accounts)
# - **Billing:** Credit balance and auto-recharge configuration
#
# **AI Integration Note:** This endpoint is designed for AI agents and MCP servers
# to understand account constraints before taking actions. Call this endpoint
# first when planning batch operations to avoid hitting limits unexpectedly.
#
# **Quick Reference:**
#
# - `GET /limits` - Get current rate limits and send limits
# - `GET /usage` - (Deprecated) Use `/limits` instead
# @return [ArkEmail::Resources::Limits]
attr_reader :limits

# Manage tenants (your customers).
#
# Create a tenant for each of your customers to track their email sending
# separately. Store the tenant `id` in your database and use `metadata` for any
# custom data.
#
# **Quick Reference:**
#
# - `POST /tenants` - Create a new tenant
# - `GET /tenants` - List all tenants (paginated)
# - `GET /tenants/{id}` - Get tenant details
# - `PATCH /tenants/{id}` - Update tenant name, metadata, or status
# - `DELETE /tenants/{id}` - Delete a tenant
# @return [ArkEmail::Resources::Tenants]
attr_reader :tenants

Expand Down
56 changes: 50 additions & 6 deletions lib/ark_email/internal/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def coerce_hash!(input)
in Hash | nil => coerced
coerced
else
message = "Expected a #{Hash} or #{ArkEmail::Internal::Type::BaseModel}, got #{data.inspect}"
message = "Expected a #{Hash} or #{ArkEmail::Internal::Type::BaseModel}, got #{input.inspect}"
raise ArgumentError.new(message)
end
end
Expand Down Expand Up @@ -237,6 +237,11 @@ def dig(data, pick, &blk)
end
end

# @type [Regexp]
#
# https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3
RFC_3986_NOT_PCHARS = /[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/

class << self
# @api private
#
Expand All @@ -247,6 +252,15 @@ def uri_origin(uri)
"#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}"
end

# @api private
#
# @param path [String, Integer]
#
# @return [String]
def encode_path(path)
path.to_s.gsub(ArkEmail::Internal::Util::RFC_3986_NOT_PCHARS) { ERB::Util.url_encode(_1) }
end

# @api private
#
# @param path [String, Array<String>]
Expand All @@ -259,7 +273,7 @@ def interpolate_path(path)
in []
""
in [String => p, *interpolations]
encoded = interpolations.map { ERB::Util.url_encode(_1) }
encoded = interpolations.map { encode_path(_1) }
format(p, *encoded)
end
end
Expand Down Expand Up @@ -490,6 +504,37 @@ def writable_enum(&blk)
JSONL_CONTENT = %r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)}

class << self
# @api private
#
# @param query [Hash{Symbol=>Object}]
#
# @return [Hash{Symbol=>Object}]
def encode_query_params(query)
out = {}
query.each { write_query_param_element!(out, _1, _2) }
out
end

# @api private
#
# @param collection [Hash{Symbol=>Object}]
# @param key [String]
# @param element [Object]
#
# @return [nil]
private def write_query_param_element!(collection, key, element)
case element
in Hash
element.each do |name, value|
write_query_param_element!(collection, "#{key}[#{name}]", value)
end
in Array
collection[key] = element.map(&:to_s).join(",")
else
collection[key] = element.to_s
end
end

# @api private
#
# @param y [Enumerator::Yielder]
Expand Down Expand Up @@ -540,16 +585,15 @@ class << self
y << "Content-Disposition: form-data"

unless key.nil?
name = ERB::Util.url_encode(key.to_s)
y << "; name=\"#{name}\""
y << "; name=\"#{key}\""
end

case val
in ArkEmail::FilePart unless val.filename.nil?
filename = ERB::Util.url_encode(val.filename)
filename = encode_path(val.filename)
y << "; filename=\"#{filename}\""
in Pathname | IO
filename = ERB::Util.url_encode(::File.basename(val.to_path))
filename = encode_path(::File.basename(val.to_path))
y << "; filename=\"#{filename}\""
else
end
Expand Down
10 changes: 9 additions & 1 deletion lib/ark_email/models/email_list_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ class EmailListResponse < ArkEmail::Internal::Type::BaseModel
# @return [String]
required :subject, String

# @!attribute tenant_id
# The tenant ID this email belongs to
#
# @return [String]
required :tenant_id, String, api_name: :tenantId

# @!attribute timestamp
#
# @return [Float]
Expand All @@ -53,7 +59,7 @@ class EmailListResponse < ArkEmail::Internal::Type::BaseModel
# @return [String, nil]
optional :tag, String

# @!method initialize(id:, from:, status:, subject:, timestamp:, timestamp_iso:, to:, tag: nil)
# @!method initialize(id:, from:, status:, subject:, tenant_id:, timestamp:, timestamp_iso:, to:, tag: nil)
# Some parameter documentations has been truncated, see
# {ArkEmail::Models::EmailListResponse} for more details.
#
Expand All @@ -65,6 +71,8 @@ class EmailListResponse < ArkEmail::Internal::Type::BaseModel
#
# @param subject [String]
#
# @param tenant_id [String] The tenant ID this email belongs to
#
# @param timestamp [Float]
#
# @param timestamp_iso [Time]
Expand Down
Loading