paubox 0.3.2 β†’ 1.0.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: 3733459b49f6166432d85899d4ea38e9d9f983c6f1ef42d17ed430fc7095beb9
4
- data.tar.gz: f4c0bf54df9f438dd499d178ca67eadb2b4f7e0aa3516089da2faed70b6f02cb
3
+ metadata.gz: da4b3dae43dc155032452be9913d48e4da7ba65878067923af62bb5497783ebc
4
+ data.tar.gz: f7e0db657ed86db45207f3c0fcd280febd834262ebcac0313a5c48c0b7e2f186
5
5
  SHA512:
6
- metadata.gz: 66acb7c648c2ede8380ecdaeb60c8cde92cd97ed66f17dce81bf8ef9e175c9d53f777fab4d1527ee3c8ceb35d65b3975a74fa5047af8add6af90380fc8b17f5b
7
- data.tar.gz: 3eeb0122292eb25f34dc24e5e62ed696200c8efd45304cde5a92467dec5c03bc0c27d01ff369ece52b4c6091c36651e886d119049922f752cafb2cc4c1c4ce97
6
+ metadata.gz: b450e2ca12166a1689fc460ec810c11dc6ad53cc2d1e71d29a06f701db91f901d2ead1ca4eb7c61f91ebb444c5caadcbc49c8a5f81157a256514bf90f4b33ac0
7
+ data.tar.gz: db478b8679f6acb5a5851b80826f81a1dd0b8360250acd68254812e4cf75c5f4bef83f21c8ec939bd43ec339e0489a554c4bfe271cc246719afcbb93816668c3
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Fail CI when a green rspec exit code does not mean the suite ran.
5
+ #
6
+ # A passing run is not evidence of coverage: an error outside an example, a
7
+ # stray tag filter, or a spec_helper that stops loading files can leave rspec
8
+ # reporting success having executed almost nothing. The failure mode is
9
+ # invisible precisely because the exit code is 0.
10
+
11
+ require 'json'
12
+
13
+ # Floor, not a target. Only trips if coverage regresses; adding specs is free.
14
+ MIN_EXAMPLES = 250
15
+
16
+ summary = JSON.parse(File.read(ARGV.fetch(0))).fetch('summary')
17
+
18
+ total = summary.fetch('example_count')
19
+ pending = summary.fetch('pending_count')
20
+ failures = summary.fetch('failure_count')
21
+ errors = summary.fetch('errors_outside_of_examples_count')
22
+ executed = total - pending
23
+
24
+ puts format(
25
+ 'examples=%d executed=%d failures=%d pending=%d errors_outside_examples=%d',
26
+ total, executed, failures, pending, errors
27
+ )
28
+
29
+ problems = []
30
+ problems << "#{errors} error(s) outside of examples" if errors.positive?
31
+ if executed < MIN_EXAMPLES
32
+ problems << "only #{executed} example(s) executed, expected at least #{MIN_EXAMPLES}"
33
+ end
34
+
35
+ unless problems.empty?
36
+ warn "FAIL: #{problems.join('; ')}"
37
+ exit 1
38
+ end
39
+
40
+ puts "OK: suite executed #{executed} examples"
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [master]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ spec:
13
+ name: rspec (ruby ${{ matrix.ruby }})
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby: ["3.1", "3.2", "3.3", "3.4"]
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ # Resolves bundler from the gemspec's own '~> 2.0' constraint and
27
+ # caches the installed gems.
28
+ bundler-cache: true
29
+
30
+ - name: Run specs
31
+ # Both formatters are explicit: a CLI --format overrides the one in
32
+ # .rspec, so without this the log would be empty on failure. The JSON
33
+ # file feeds the gate below.
34
+ run: >-
35
+ bundle exec rspec
36
+ --format documentation
37
+ --format json --out rspec-results.json
38
+
39
+ - name: Assert the suite actually executed
40
+ run: ruby .github/scripts/assert_specs_ran.rb rspec-results.json
@@ -0,0 +1,36 @@
1
+ name: PR Title
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [master, main]
6
+ types: [opened, edited, reopened, synchronize]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ conventional-title:
13
+ name: Conventional commit title
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ # With squash merging the squash commit subject is the PR title, so this
17
+ # string is all release-please ever parses. A title it cannot parse is
18
+ # silently dropped from the release rather than failing anything, so it is
19
+ # gated here instead.
20
+ - uses: amannn/action-semantic-pull-request@v6
21
+ env:
22
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23
+ with:
24
+ types: |
25
+ feat
26
+ fix
27
+ perf
28
+ revert
29
+ docs
30
+ style
31
+ refactor
32
+ test
33
+ build
34
+ ci
35
+ chore
36
+ deps
@@ -0,0 +1,88 @@
1
+ name: release-please
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ name: Release Please
14
+ runs-on: ubuntu-latest
15
+ outputs:
16
+ release_created: ${{ steps.release.outputs.release_created }}
17
+ tag_name: ${{ steps.release.outputs.tag_name }}
18
+ steps:
19
+ # Maintains a release PR that bumps VERSION in lib/paubox/version.rb and
20
+ # writes CHANGELOG.md. Merging that PR creates the tag and the GitHub
21
+ # release, which gates the publish job below.
22
+ #
23
+ # The manifest is seeded from RubyGems (0.3.2), not from version.rb, which
24
+ # had drifted to 0.4.0. Neither 0.4.0 nor 0.3.1 was ever pushed, so
25
+ # seeding from the repo would have skipped two unused numbers.
26
+ #
27
+ # include-component-in-tag is false so tags stay bare `vX.Y.Z`, matching
28
+ # the existing v0.3.0 and v0.3.2 and the rest of the SDK fleet.
29
+ - uses: googleapis/release-please-action@v4
30
+ id: release
31
+ with:
32
+ token: ${{ secrets.GITHUB_TOKEN }}
33
+ config-file: release-please-config.json
34
+ manifest-file: .release-please-manifest.json
35
+
36
+ publish:
37
+ name: Publish ${{ needs.release-please.outputs.tag_name }} to RubyGems
38
+ needs: release-please
39
+ if: needs.release-please.outputs.release_created == 'true'
40
+ runs-on: ubuntu-latest
41
+ permissions:
42
+ contents: read
43
+ # Required for OIDC. Without it the credentials step cannot identify
44
+ # itself to RubyGems.org and the push fails.
45
+ id-token: write
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ with:
49
+ ref: ${{ needs.release-please.outputs.tag_name }}
50
+ # The gemspec builds its file list from `git ls-files`, so the
51
+ # checkout has to keep the repository intact.
52
+ persist-credentials: false
53
+
54
+ - uses: ruby/setup-ruby@v1
55
+ with:
56
+ ruby-version: '3.4'
57
+ bundler-cache: true
58
+
59
+ - name: Run specs
60
+ run: bundle exec rspec --format documentation
61
+
62
+ - name: Verify the tag and version.rb agree
63
+ # Cheap guard against pushing a gem whose version does not match the
64
+ # tag it was cut from. RubyGems only allows a yank within 72 hours and
65
+ # never allows the number to be reused.
66
+ env:
67
+ RELEASE_TAG: ${{ needs.release-please.outputs.tag_name }}
68
+ run: |
69
+ GEM_VERSION=$(ruby -Ilib -e 'require "paubox/version"; print Paubox::VERSION')
70
+ if [ "v${GEM_VERSION}" != "${RELEASE_TAG}" ]; then
71
+ echo "::error::version.rb is ${GEM_VERSION} but the tag is ${RELEASE_TAG}"
72
+ exit 1
73
+ fi
74
+ echo "version.rb ${GEM_VERSION} matches ${RELEASE_TAG}"
75
+
76
+ - name: Build the gem
77
+ run: gem build paubox_ruby.gemspec
78
+
79
+ # Trusted publishing: exchanges GitHub's OIDC token for a short-lived
80
+ # RubyGems credential. There is no API key stored anywhere. RubyGems pins
81
+ # the trust to the repository and this workflow filename, so renaming
82
+ # this file breaks publishing until the trusted publisher entry for the
83
+ # `paubox` gem is updated to match.
84
+ - name: Configure RubyGems credentials
85
+ uses: rubygems/configure-rubygems-credentials@main
86
+
87
+ - name: Push the gem
88
+ run: gem push paubox-*.gem
data/.gitignore CHANGED
@@ -11,4 +11,7 @@
11
11
 
12
12
  # rspec failure tracking
13
13
  .rspec_status
14
- .gem
14
+ .gem
15
+
16
+ # rspec JSON output written by CI.
17
+ rspec-results.json
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.0.0"
3
+ }
data/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.0](https://github.com/Paubox/paubox-ruby/compare/v0.3.2...v1.0.0) (2026-08-21)
6
+
7
+ First stable release. RubyGems had been on `0.3.2` since October 2022.
8
+
9
+ ### ⚠ BREAKING CHANGES
10
+
11
+ - Promotes the gem from `0.x` to a stable `1.0.0`. No public API is removed and existing code keeps working, but dependents pinned with a pessimistic `0.x` constraint will not resolve `1.0.0` without widening it. This affects `paubox_rails`, whose gemspec requires `paubox '~> 0.3'`
12
+
13
+ ### πŸš€ New Features
14
+
15
+ - Add `Paubox::FormsClient` for the Paubox Forms API, with `Paubox::Form` and `Paubox::FormSubmission` models
16
+ - Public endpoints, no credential attached: `get_form`, `submit_form`
17
+ - Form management with a scoped API key (`forms` scope, sent as `Authorization: Bearer <key>`): `list_forms`, `find_form`, `create_form`, `update_form`, `archive_form`, `unarchive_form`, `copy_form`, `form_stats`
18
+ - Submissions: `list_submissions`, `submissions_csv`, `submission_pdf`
19
+ - The Email API no longer requires `api_user` β€” an API key alone authenticates
20
+
21
+ ### ⚠️ Behavior Changes
22
+
23
+ - Email API base URLs move to `api.paubox.com`. `api_user` is accepted and ignored, and `Paubox::Client#api_user` is kept as a deprecated reader, so existing configuration keeps working
24
+ - The Forms API host is `api.paubox.com`, moved from the earlier `apx.paubox.com/forms`
25
+
26
+ ### πŸ”’ Hardening
27
+
28
+ - Validate and encode caller-supplied values interpolated into Forms request paths. Authenticated endpoints require a UUID; public endpoints encode the segment instead of rejecting it
29
+ - Keep the Bearer token out of exception messages raised from Forms requests
30
+
31
+ ### πŸ› Fixes
32
+
33
+ - Declare `base64` and `ostruct` as runtime dependencies. Both left the Ruby default gems (`base64` in 3.4, `ostruct` in 4.0) and `lib/` requires them, so the gem failed to load on Ruby 3.4 without them ([7299324](https://github.com/Paubox/paubox-ruby/commit/729932436a8982c11f2a71d0d2327265160317d3))
34
+
35
+ ### πŸŽ‰ Enhancements
36
+
37
+ - Replace the dead Travis config with a GitHub Actions CI workflow running RSpec on Ruby 3.1 through 3.4
38
+
39
+ ## v0.3.2 / 2022-10-06
40
+
41
+ ### πŸš€ New Features
42
+
43
+ - Add support for sending messages using dynamic templates ([#8](https://github.com/Paubox/paubox-ruby/pull/8))
44
+
45
+ ## v0.3.0 / 2019-07-10
46
+
47
+ ### πŸŽ‰ Enhancements
48
+
49
+ - Version bump and dependency maintenance
50
+
51
+ ## v0.2.3 / 2018-10-04
52
+
53
+ ### πŸŽ‰ Enhancements
54
+
55
+ - Relax the `mail` dependency to `>= 2.5`
56
+ - Declare a minimum Ruby version of 2.3
57
+
58
+ ## v0.1.3 / 2018-04-30
59
+
60
+ ### πŸŽ‰ Enhancements
61
+
62
+ - Relax the `mail` dependency to `>= 2.6`
63
+
64
+ ## v0.1.1 / 2018-04-26
65
+
66
+ ### πŸ› Fixes
67
+
68
+ - Packaging fixes following the initial release
69
+
70
+ ## v0.1.0 / 2018-04-17
71
+
72
+ ### πŸš€ Major Release
73
+
74
+ First release of the Paubox Transactional Email SDK for Ruby.
data/CLAUDE.md ADDED
@@ -0,0 +1,114 @@
1
+ # Paubox Ruby Gem β€” Developer Context
2
+
3
+ ## Purpose
4
+
5
+ This is the official Ruby gem for the Paubox platform. It provides:
6
+ - **Email API**: send HIPAA-compliant email, manage dynamic templates, check delivery status
7
+ - **Forms API**: fetch form definitions, submit form responses, and manage forms and submissions
8
+
9
+ ## Directory Structure
10
+
11
+ ```
12
+ lib/
13
+ paubox.rb # Entry point: requires all lib files, holds Configuration
14
+ paubox_ruby.rb # Alias for paubox.rb
15
+ paubox/
16
+ version.rb # Gem version constant
17
+ client.rb # Email API client (authenticated, api.paubox.com)
18
+ forms_client.rb # Forms API client (api.paubox.com/forms; Bearer auth on management endpoints)
19
+ message.rb # Builds send-message API payload from a hash
20
+ templated_message.rb # Extends Message for template-based sends
21
+ mail_to_message.rb # Adapts Ruby Mail::Message to API payload
22
+ dynamic_templates.rb # CRUD for dynamic email templates
23
+ email_disposition.rb # Parses delivery/open status responses
24
+ form.rb # Parses form metadata responses
25
+ form_submission.rb # Parses form submission responses
26
+ format_helper.rb # Shared utilities: base64, key mapping, normalization
27
+ mail/
28
+ paubox.rb # Plugs Paubox into Ruby Mail as a delivery method
29
+
30
+ spec/
31
+ spec_helper.rb # RSpec + WebMock setup
32
+ paubox/ # Unit specs per class
33
+ mail/ # Specs for Ruby Mail integration
34
+ helpers/ # Shared fixtures (MessageHelper, FormHelper, etc.)
35
+ ```
36
+
37
+ ## Key Classes
38
+
39
+ | Class | Responsibility |
40
+ |---|---|
41
+ | `Paubox::Client` | Authenticated HTTP client for the Email API. Token auth via `Authorization: Token token=<key>`. Base URL: `https://api.paubox.com/v1`. |
42
+ | `Paubox::FormsClient` | HTTP client for the Forms API. Base URL: `https://api.paubox.com/forms`. Public endpoints (`get_form`, `submit_form`) send no auth headers; management endpoints (list/create/find/update/archive/copy forms, stats, submissions, CSV/PDF export) require a "forms"-scoped API key sent as `Authorization: Bearer <api_key>`. |
43
+ | `Paubox::Message` | Builds the JSON payload for `/messages`. Accepts `from`, `to`, `cc`, `bcc`, `subject`, `text_content`, `html_content`, `attachments`. |
44
+ | `Paubox::TemplatedMessage` | Extends `Message`; overrides `send_message_payload` to include `template_name` / `template_values`. |
45
+ | `Paubox::MailToMessage` | Converts a `Mail::Message` object into a Paubox API payload. |
46
+ | `Paubox::DynamicTemplates` | Manages template CRUD via class methods (`create`, `list`, `find`) and instance methods (`update`, `delete`). |
47
+ | `Paubox::EmailDisposition` | Parses the `/message_receipt` response into `MessageDelivery` and `MessageDeliveryStatus` structs. |
48
+ | `Paubox::Form` | Parses form responses (`/public/form_data/<id>`, `/api/forms` endpoints). Exposes predicate methods: `active?`, `deleted?`, `archived?`, `signable?`. |
49
+ | `Paubox::FormSubmission` | Parses a form submission from `/api/forms/<form_id>/submissions`. Exposes `id`, `form_id`, `form_data` (JSON string parsed to a Hash), `submitter_email`, `recipients`, attachment fields, `created_at`. |
50
+ | `Paubox::FormatHelper` | Mixed into message builders; handles base64 encoding, snake_case→camelCase key mapping, email list normalization. |
51
+ | `Mail::Paubox` | Delivery method for the Ruby Mail library. Delegates to `Paubox::Client`. |
52
+
53
+ ## Adding a New Feature
54
+
55
+ ### New API endpoint on the Email API
56
+ 1. Add a method to `Paubox::Client` that calls `send_request` or `RestClient` directly.
57
+ 2. If the response needs a structured model, create `lib/paubox/<model>.rb` following `EmailDisposition` as a pattern.
58
+ 3. Require the new file in `lib/paubox.rb`.
59
+ 4. Add specs in `spec/paubox/<feature>_spec.rb` using WebMock to stub HTTP.
60
+
61
+ ### New API endpoint on the Forms API
62
+ 1. Add a method to `Paubox::FormsClient`.
63
+ 2. If needed, add a response model following `Paubox::Form`.
64
+ 3. Require in `lib/paubox.rb`.
65
+ 4. Add specs in `spec/paubox/forms_client_spec.rb` or a new file.
66
+
67
+ ## Testing
68
+
69
+ **Framework:** RSpec 3 + WebMock
70
+
71
+ ```bash
72
+ bundle exec rspec # run all specs
73
+ bundle exec rspec spec/paubox/form_spec.rb # run a single file
74
+ ```
75
+
76
+ WebMock stubs outbound HTTP. All specs must stub any HTTP calls they trigger β€” no real network requests are made.
77
+
78
+ Fixtures live in `spec/helpers/` as includable modules (e.g. `Helpers::FormHelper`, `Helpers::MessageHelper`). Include them per spec file with `RSpec.configure { |c| c.include Helpers::XHelper }`.
79
+
80
+ ## Authentication
81
+
82
+ - **Email API**: `Authorization: Token token=<api_key>` header on every request. The API key alone authenticates β€” no username/user segment is needed. Configured via `Paubox.configure` or `Paubox::Client.new(api_key:)`. `Configuration` keeps a deprecated `api_user` accessor for backward compatibility; it is ignored.
83
+ - **Forms API**: Public endpoints (`get_form`, `submit_form`) need no auth β€” `Paubox::FormsClient` sends no auth headers for them. Management endpoints require an API key with the "forms" scope (validated server-side, not by the gem), sent as `Authorization: Bearer <api_key>`. This is a separate key from the Email API key: configure it via `Paubox.configure { |c| c.forms_api_key = ... }` or `Paubox::FormsClient.new(api_key:)` β€” the client never falls back to `Paubox.configuration.api_key`. Calling a management endpoint without a key raises `ArgumentError`. Note: `list_forms` requires `customer_id` (must match the API key's customer, enforced server-side; the gem raises `ArgumentError` if it is missing).
84
+
85
+ ## Dependencies
86
+
87
+ - `rest-client` (~> 2.0) β€” HTTP client
88
+ - `mail` (>= 2.5) β€” Ruby Mail integration
89
+
90
+ ## Releases
91
+
92
+ Releases are automated with [release-please](https://github.com/googleapis/release-please). Merging to `master` refreshes a standing release PR; merging *that* PR bumps `lib/paubox/version.rb`, writes `CHANGELOG.md`, creates a bare `vX.Y.Z` tag and a GitHub release, and then **pushes the gem to RubyGems**.
93
+
94
+ Do **not** hand-edit `VERSION` or add a `CHANGELOG.md` entry β€” release-please owns both.
95
+
96
+ The next version comes from PR titles, so the title is the only thing that matters: `feat:` gives a minor bump, `fix:` a patch, and a `!` suffix or a `BREAKING CHANGE:` footer gives a major. `.github/workflows/pr-title.yml` rejects titles release-please cannot parse.
97
+
98
+ To force a specific version, land an empty commit carrying a `Release-As` footer. Put the release notes in that commit's body β€” a bare `chore: release X` produces an empty changelog entry, because the commits it would otherwise draw from get dropped if they are not conventional:
99
+
100
+ ```bash
101
+ git commit --allow-empty -m "chore: release 1.0.0" -m "Release-As: 1.0.0"
102
+ ```
103
+
104
+ ### Publishing
105
+
106
+ Publishing uses RubyGems **trusted publishing** (OIDC) β€” there is no API key stored anywhere. RubyGems pins the trust to the repository and the workflow filename, so **renaming `release-please.yml` breaks publishing** until the trusted publisher entry for the `paubox` gem is updated to match.
107
+
108
+ If a push fails, re-run the failed `publish` job from the Actions tab. Note that a re-run uses the workflow file from the original commit, so it only helps when the fix is on the RubyGems side; a fix to the workflow itself needs a new release.
109
+
110
+ Version numbers on RubyGems are effectively permanent β€” a yank is only possible within 72 hours and never frees the number for reuse.
111
+
112
+ ### Relationship to `paubox_rails`
113
+
114
+ `paubox_rails` depends on this gem. Its gemspec constraint has to allow whatever major version is current here, so a major bump in `paubox` requires a matching `paubox_rails` release that widens the constraint. Release `paubox` first.