paubox 1.0.0 → 1.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: da4b3dae43dc155032452be9913d48e4da7ba65878067923af62bb5497783ebc
4
- data.tar.gz: f7e0db657ed86db45207f3c0fcd280febd834262ebcac0313a5c48c0b7e2f186
3
+ metadata.gz: 5ba4ee3af31174ce6a77fb403a770afe36a35bbdb36b04358d613d8e7762bbf0
4
+ data.tar.gz: c89efb57681fa92d3957f27df88a95b54f121b1c13cd6f6b25fb69751482d9bf
5
5
  SHA512:
6
- metadata.gz: b450e2ca12166a1689fc460ec810c11dc6ad53cc2d1e71d29a06f701db91f901d2ead1ca4eb7c61f91ebb444c5caadcbc49c8a5f81157a256514bf90f4b33ac0
7
- data.tar.gz: db478b8679f6acb5a5851b80826f81a1dd0b8360250acd68254812e4cf75c5f4bef83f21c8ec939bd43ec339e0489a554c4bfe271cc246719afcbb93816668c3
6
+ metadata.gz: 64f70fb89c5a13f75fcdc376742389a8fb27dec34f1a25f8f480c357fb8680b70d35429d3622de55daa626aa62b61a19e17ed945adc7e3eced25c36e73d63234
7
+ data.tar.gz: 6aa4d4a5ef992be5cc041a7f09a9ceb7ecaa412f085028ce1d26ebfe8411c099e688d79906efff72ebae74d4cfd412b006b276673ec550fcd2d6b966dbef6917
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ # Probes each base URL with a junk token: api.paubox.com answers unrouted paths
3
+ # with its own HTML 404 page, so a wrong base fails here without credentials.
4
+ #
5
+ # Usage: route-check.sh <url> [url ...]
6
+ # <command printing one URL per line> | route-check.sh
7
+ set -eu
8
+
9
+ MARKER='The resource could not be found'
10
+ TOKEN='route-check-not-a-real-key'
11
+
12
+ urls=$(mktemp)
13
+ fails=$(mktemp)
14
+ trap 'rm -f "$urls" "$fails"' EXIT
15
+
16
+ if [ "$#" -gt 0 ]; then
17
+ for u in "$@"; do echo "$u"; done > "$urls"
18
+ else
19
+ cat > "$urls"
20
+ fi
21
+
22
+ sed 's#/*$##' "$urls" | grep '^https://' | sort -u > "$urls.clean" || true
23
+ mv "$urls.clean" "$urls"
24
+
25
+ if [ ! -s "$urls" ]; then
26
+ echo "route-check: no URLs given" >&2
27
+ exit 1
28
+ fi
29
+
30
+ while IFS= read -r url; do
31
+ if curl -sS -m 20 -H "Authorization: Token token=$TOKEN" "$url" \
32
+ | grep -qF "$MARKER"; then
33
+ echo "FAIL $url <- not routed"
34
+ echo "$url" >> "$fails"
35
+ else
36
+ echo "ok $url"
37
+ fi
38
+ done < "$urls"
39
+
40
+ if [ -s "$fails" ]; then
41
+ echo "See https://docs.paubox.com for the documented base URLs." >&2
42
+ exit 1
43
+ fi
@@ -38,3 +38,24 @@ jobs:
38
38
 
39
39
  - name: Assert the suite actually executed
40
40
  run: ruby .github/scripts/assert_specs_ran.rb rspec-results.json
41
+
42
+ route-check:
43
+ name: Base URLs are routed
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+
48
+ - uses: ruby/setup-ruby@v1
49
+ with:
50
+ ruby-version: "3.3"
51
+ bundler-cache: true
52
+
53
+ # Ask the client, so a base URL change cannot pass unprobed.
54
+ - name: Probe the base URLs the clients build
55
+ run: |
56
+ bundle exec ruby -Ilib -rjson -rpaubox -e '
57
+ Paubox.configure { |c| c.api_key = "route-check" }
58
+ puts Paubox::Client.new.send(:api_base_endpoint)
59
+ f = Paubox::FormsClient
60
+ puts "#{f::FORMS_PROTOCOL}#{f::FORMS_HOST}#{f::FORMS_BASE}"
61
+ ' | .github/scripts/route-check.sh
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.0.0"
2
+ ".": "1.2.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.2.0](https://github.com/Paubox/paubox-ruby/compare/v1.1.0...v1.2.0) (2026-09-16)
6
+
7
+
8
+ ### Features
9
+
10
+ * add receiving (inbound email) endpoints ([#29](https://github.com/Paubox/paubox-ruby/issues/29)) ([aeda949](https://github.com/Paubox/paubox-ruby/commit/aeda9497215ce6d701af7eb37ec9ac895910e1b2))
11
+
12
+ ## [1.1.0](https://github.com/Paubox/paubox-ruby/compare/v1.0.0...v1.1.0) (2026-09-09)
13
+
14
+
15
+ ### Features
16
+
17
+ * add scheduled send support ([#26](https://github.com/Paubox/paubox-ruby/issues/26)) ([9ddb07a](https://github.com/Paubox/paubox-ruby/commit/9ddb07a20f41874a10be6e0479cb943e833b336d))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * target the documented /v1/email and /v1/forms base URLs ([#24](https://github.com/Paubox/paubox-ruby/issues/24)) ([3a32459](https://github.com/Paubox/paubox-ruby/commit/3a324596ef8e9237d5da15246b36a21baf3df688))
23
+
5
24
  ## [1.0.0](https://github.com/Paubox/paubox-ruby/compare/v0.3.2...v1.0.0) (2026-08-21)
6
25
 
7
26
  First stable release. RubyGems had been on `0.3.2` since October 2022.
data/CLAUDE.md CHANGED
@@ -15,7 +15,7 @@ lib/
15
15
  paubox/
16
16
  version.rb # Gem version constant
17
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)
18
+ forms_client.rb # Forms API client (api.paubox.com/v1/forms; Bearer auth on management endpoints)
19
19
  message.rb # Builds send-message API payload from a hash
20
20
  templated_message.rb # Extends Message for template-based sends
21
21
  mail_to_message.rb # Adapts Ruby Mail::Message to API payload
@@ -38,8 +38,8 @@ spec/
38
38
 
39
39
  | Class | Responsibility |
40
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>`. |
41
+ | `Paubox::Client` | Authenticated HTTP client for the Email API. Token auth via `Authorization: Token token=<key>`. Base URL: `https://api.paubox.com/v1/email`. |
42
+ | `Paubox::FormsClient` | HTTP client for the Forms API. Base URL: `https://api.paubox.com/v1/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
43
  | `Paubox::Message` | Builds the JSON payload for `/messages`. Accepts `from`, `to`, `cc`, `bcc`, `subject`, `text_content`, `html_content`, `attachments`. |
44
44
  | `Paubox::TemplatedMessage` | Extends `Message`; overrides `send_message_payload` to include `template_name` / `template_values`. |
45
45
  | `Paubox::MailToMessage` | Converts a `Mail::Message` object into a Paubox API payload. |
data/api.md CHANGED
@@ -28,7 +28,7 @@ Authorization: Token token=<api_key>
28
28
  ### Base URL
29
29
 
30
30
  ```
31
- https://api.paubox.com/v1
31
+ https://api.paubox.com/v1/email
32
32
  ```
33
33
 
34
34
  All Email API paths below are relative to this base.
@@ -178,7 +178,7 @@ Returns service health. No authentication required in practice.
178
178
  ### Base URL
179
179
 
180
180
  ```
181
- https://api.paubox.com/forms
181
+ https://api.paubox.com/v1/forms
182
182
  ```
183
183
 
184
184
  ---
data/lib/paubox/client.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Paubox
4
4
  # Client sends API requests to Paubox API
5
5
  class Client
6
+ require 'cgi'
6
7
  require 'rest-client'
7
8
  require 'ostruct'
8
9
  attr_reader :api_key, :api_host, :api_protocol, :api_version
@@ -54,6 +55,113 @@ module Paubox
54
55
  end
55
56
  alias message_receipt email_disposition
56
57
 
58
+ def schedule_mail(mail, scheduled_at)
59
+ case mail
60
+ when Mail::Message
61
+ allow_non_tls = mail.allow_non_tls.nil? ? false : mail.allow_non_tls
62
+ msg_payload = MailToMessage.new(mail, allow_non_tls: allow_non_tls).send_message_payload
63
+ msg_data = JSON.parse(msg_payload)['data']['message']
64
+ when Hash
65
+ msg_data = Message.new(mail).send_message_payload
66
+ msg_data = JSON.parse(msg_data)['data']['message']
67
+ when Paubox::Message
68
+ msg_data = JSON.parse(mail.send_message_payload)['data']['message']
69
+ end
70
+ payload = { data: { message: msg_data, scheduled_at: scheduled_at } }.to_json
71
+ url = request_endpoint('schedule')
72
+ response = RestClient.post(url, payload, auth_header)
73
+ JSON.parse(response.body)
74
+ end
75
+
76
+ def get_scheduled(source_tracking_id)
77
+ url = request_endpoint("schedule/#{source_tracking_id}")
78
+ response = RestClient.get(url, auth_header)
79
+ JSON.parse(response.body)
80
+ end
81
+
82
+ def reschedule(source_tracking_id, scheduled_at)
83
+ url = request_endpoint("schedule/#{source_tracking_id}")
84
+ payload = { scheduled_at: scheduled_at }.to_json
85
+ response = RestClient.patch(url, payload, auth_header)
86
+ JSON.parse(response.body)
87
+ end
88
+
89
+ def cancel_scheduled(source_tracking_id)
90
+ url = request_endpoint("schedule/#{source_tracking_id}/cancel")
91
+ response = RestClient.post(url, nil, auth_header)
92
+ JSON.parse(response.body)
93
+ end
94
+
95
+ def list_receiving_domains
96
+ url = request_endpoint('receiving/domains')
97
+ response = RestClient.get(url, auth_header)
98
+ JSON.parse(response.body)
99
+ end
100
+
101
+ def create_receiving_domain(slug: nil)
102
+ url = request_endpoint('receiving/domains')
103
+ payload = { slug: slug }.compact.to_json
104
+ response = RestClient.post(url, payload, auth_header)
105
+ JSON.parse(response.body)
106
+ end
107
+
108
+ def get_receiving_domain(id)
109
+ url = request_endpoint("receiving/domains/#{id}")
110
+ response = RestClient.get(url, auth_header)
111
+ JSON.parse(response.body)
112
+ end
113
+
114
+ def delete_receiving_domain(id)
115
+ url = request_endpoint("receiving/domains/#{id}")
116
+ response = RestClient.delete(url, auth_header)
117
+ JSON.parse(response.body)
118
+ end
119
+
120
+ def list_receiving_mailboxes(domain_id)
121
+ url = request_endpoint("receiving/domains/#{domain_id}/mailboxes")
122
+ response = RestClient.get(url, auth_header)
123
+ JSON.parse(response.body)
124
+ end
125
+
126
+ def create_receiving_mailbox(domain_id, name:, password:, quota_bytes: nil)
127
+ url = request_endpoint("receiving/domains/#{domain_id}/mailboxes")
128
+ payload = { name: name, password: password, quota_bytes: quota_bytes }.compact.to_json
129
+ response = RestClient.post(url, payload, auth_header)
130
+ JSON.parse(response.body)
131
+ end
132
+
133
+ def get_receiving_mailbox(domain_id, mailbox_id)
134
+ url = request_endpoint("receiving/domains/#{domain_id}/mailboxes/#{mailbox_id}")
135
+ response = RestClient.get(url, auth_header)
136
+ JSON.parse(response.body)
137
+ end
138
+
139
+ def delete_receiving_mailbox(domain_id, mailbox_id)
140
+ url = request_endpoint("receiving/domains/#{domain_id}/mailboxes/#{mailbox_id}")
141
+ response = RestClient.delete(url, auth_header)
142
+ JSON.parse(response.body)
143
+ end
144
+
145
+ def list_received_emails(limit: nil, after: nil, before: nil)
146
+ params = { limit: limit, after: after, before: before }.compact
147
+ query = params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
148
+ url = request_endpoint('receiving')
149
+ url = "#{url}?#{query}" unless query.empty?
150
+ response = RestClient.get(url, auth_header)
151
+ JSON.parse(response.body)
152
+ end
153
+
154
+ def get_received_email(email_id)
155
+ url = request_endpoint("receiving/#{email_id}")
156
+ response = RestClient.get(url, auth_header)
157
+ JSON.parse(response.body)
158
+ end
159
+
160
+ def get_received_email_attachment(email_id, blob_id)
161
+ url = request_endpoint("receiving/#{email_id}/attachments/#{blob_id}")
162
+ RestClient.get(url, auth_header)
163
+ end
164
+
57
165
  def send_request(method: :get, payload: {}, path: '')
58
166
  url = request_endpoint(path)
59
167
 
@@ -69,7 +177,7 @@ module Paubox
69
177
  end
70
178
 
71
179
  def api_base_endpoint
72
- "#{api_protocol}#{api_host}/#{api_version}"
180
+ "#{api_protocol}#{api_host}/#{api_version}/email"
73
181
  end
74
182
 
75
183
  def request_endpoint(endpoint)
@@ -7,7 +7,7 @@ module Paubox
7
7
  class FormsClient
8
8
  FORMS_HOST = 'api.paubox.com'
9
9
  FORMS_PROTOCOL = 'https://'
10
- FORMS_BASE = '/forms'
10
+ FORMS_BASE = '/v1/forms'
11
11
 
12
12
  TIMEOUT_SECONDS = 30
13
13
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paubox
4
- VERSION = '1.0.0'
4
+ VERSION = '1.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paubox
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paubox
@@ -149,6 +149,7 @@ extensions: []
149
149
  extra_rdoc_files: []
150
150
  files:
151
151
  - ".github/scripts/assert_specs_ran.rb"
152
+ - ".github/scripts/route-check.sh"
152
153
  - ".github/workflows/ci.yml"
153
154
  - ".github/workflows/pr-title.yml"
154
155
  - ".github/workflows/release-please.yml"