postmark_ruby_client 0.1.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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +278 -0
- data/Rakefile +24 -0
- data/lib/postmark_client/client/base.rb +163 -0
- data/lib/postmark_client/configuration.rb +79 -0
- data/lib/postmark_client/errors.rb +52 -0
- data/lib/postmark_client/models/attachment.rb +126 -0
- data/lib/postmark_client/models/email.rb +254 -0
- data/lib/postmark_client/models/email_response.rb +88 -0
- data/lib/postmark_client/resources/emails.rb +159 -0
- data/lib/postmark_client/version.rb +7 -0
- data/lib/postmark_client.rb +78 -0
- metadata +191 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4d157f97b4114e08d81c6c5a7cba8d0e5862c86ecce6cbc520b91764271a8c88
|
|
4
|
+
data.tar.gz: 248c8666681e1f6baf961c0778eae8bb976b8343c5d78a06012e1ab75bdb6cbf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c128a5537f655491133f0a5f0c58e037549234a61feeb340e802a296dca3102d17696e387af9b17269277161cf1bae598163afb0ab68b198f0bd7e951cb65cd
|
|
7
|
+
data.tar.gz: 44fcd733e4e2e2935e33d5fb86f3ee6947d37294b6369942a613aa670e77d5d4b362fc83610b31a431873958d7468c07c90516d3148b6607ce9463fa5afc4221
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-01-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release
|
|
15
|
+
- Email API support for sending single emails
|
|
16
|
+
- Email API support for batch sending (up to 500 emails)
|
|
17
|
+
- Email model with full Postmark API field support
|
|
18
|
+
- Attachment support with auto Base64 encoding
|
|
19
|
+
- File attachment helper method
|
|
20
|
+
- Inline attachment support for HTML emails
|
|
21
|
+
- Custom header support
|
|
22
|
+
- Metadata support
|
|
23
|
+
- Link and open tracking configuration
|
|
24
|
+
- Global configuration via initializer
|
|
25
|
+
- Per-request API token override
|
|
26
|
+
- Comprehensive error handling (ValidationError, ApiError, ConnectionError)
|
|
27
|
+
- Full RSpec test suite
|
|
28
|
+
- YARD documentation
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alvaro Delgado
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# postmark_ruby_client
|
|
2
|
+
|
|
3
|
+
A clean, extensible Ruby client for the [Postmark](https://postmarkapp.com) transactional email API. Built with Faraday and designed for Rails 8+ applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Simple API**: Intuitive Ruby interface for sending emails
|
|
8
|
+
- **Extensible Design**: Base client class makes it easy to add new API endpoints
|
|
9
|
+
- **Full Email Support**: HTML/text bodies, attachments, custom headers, metadata, tracking
|
|
10
|
+
- **Batch Sending**: Send up to 500 emails in a single API call
|
|
11
|
+
- **Type Safety**: Validation before API calls to catch errors early
|
|
12
|
+
- **Configurable**: Global configuration with per-request overrides
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'postmark_ruby_client'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it yourself:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install postmark_ruby_client
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
Configure the gem with your Postmark server API token. In a Rails application, create an initializer:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# config/initializers/postmark_ruby_client.rb
|
|
40
|
+
PostmarkClient.configure do |config|
|
|
41
|
+
config.api_token = ENV["POSTMARK_API_TOKEN"]
|
|
42
|
+
|
|
43
|
+
# Optional settings with defaults
|
|
44
|
+
config.default_message_stream = "outbound" # Default message stream
|
|
45
|
+
config.timeout = 30 # Request timeout in seconds
|
|
46
|
+
config.open_timeout = 10 # Connection timeout in seconds
|
|
47
|
+
config.track_opens = false # Default open tracking
|
|
48
|
+
config.track_links = "None" # Default link tracking
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The API token can also be set via the `POSTMARK_API_TOKEN` environment variable.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Sending a Simple Email
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Using the convenience method
|
|
60
|
+
response = PostmarkClient.deliver(
|
|
61
|
+
from: "sender@example.com",
|
|
62
|
+
to: "recipient@example.com",
|
|
63
|
+
subject: "Hello!",
|
|
64
|
+
text_body: "Hello, World!"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if response.success?
|
|
68
|
+
puts "Email sent! Message ID: #{response.message_id}"
|
|
69
|
+
else
|
|
70
|
+
puts "Error: #{response.message}"
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using the Email Model
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
email = PostmarkClient::Email.new(
|
|
78
|
+
from: "John Doe <john@example.com>",
|
|
79
|
+
to: ["alice@example.com", "bob@example.com"],
|
|
80
|
+
cc: "manager@example.com",
|
|
81
|
+
bcc: "archive@example.com",
|
|
82
|
+
subject: "Monthly Report",
|
|
83
|
+
html_body: "<h1>Report</h1><p>See attached.</p>",
|
|
84
|
+
text_body: "Report - See attached.",
|
|
85
|
+
reply_to: "support@example.com",
|
|
86
|
+
tag: "monthly-report",
|
|
87
|
+
track_opens: true,
|
|
88
|
+
track_links: "HtmlAndText",
|
|
89
|
+
metadata: { "client_id" => "12345" }
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
client = PostmarkClient::Resources::Emails.new
|
|
93
|
+
response = client.send(email)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Adding Attachments
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
email = PostmarkClient::Email.new(
|
|
100
|
+
from: "sender@example.com",
|
|
101
|
+
to: "recipient@example.com",
|
|
102
|
+
subject: "Files attached",
|
|
103
|
+
text_body: "Please see the attached files."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Add attachment from parameters
|
|
107
|
+
email.add_attachment(
|
|
108
|
+
name: "document.pdf",
|
|
109
|
+
content: File.binread("path/to/document.pdf"),
|
|
110
|
+
content_type: "application/pdf"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Or attach directly from a file path
|
|
114
|
+
email.attach_file("path/to/image.png")
|
|
115
|
+
|
|
116
|
+
# Inline attachments for HTML emails
|
|
117
|
+
email.html_body = '<p>Logo: <img src="cid:logo.png"/></p>'
|
|
118
|
+
email.add_attachment(
|
|
119
|
+
name: "logo.png",
|
|
120
|
+
content: File.binread("logo.png"),
|
|
121
|
+
content_type: "image/png",
|
|
122
|
+
content_id: "cid:logo.png"
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom Headers
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
email = PostmarkClient::Email.new(
|
|
130
|
+
from: "sender@example.com",
|
|
131
|
+
to: "recipient@example.com",
|
|
132
|
+
subject: "Custom headers",
|
|
133
|
+
text_body: "Hello"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
email.add_header(name: "X-Custom-Header", value: "custom-value")
|
|
137
|
+
email.add_header(name: "X-Priority", value: "1")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Batch Sending
|
|
141
|
+
|
|
142
|
+
Send up to 500 emails in a single API call:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
emails = users.map do |user|
|
|
146
|
+
{
|
|
147
|
+
from: "notifications@example.com",
|
|
148
|
+
to: user.email,
|
|
149
|
+
subject: "Your weekly digest",
|
|
150
|
+
text_body: "Here's what you missed..."
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
client = PostmarkClient::Resources::Emails.new
|
|
155
|
+
responses = client.send_batch(emails)
|
|
156
|
+
|
|
157
|
+
responses.each do |response|
|
|
158
|
+
if response.success?
|
|
159
|
+
puts "Sent to #{response.to}"
|
|
160
|
+
else
|
|
161
|
+
puts "Failed: #{response.message}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Using a Custom API Token
|
|
167
|
+
|
|
168
|
+
Override the global configuration for specific requests:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# For a different Postmark server
|
|
172
|
+
client = PostmarkClient::Resources::Emails.new(api_token: "different-token")
|
|
173
|
+
response = client.send(email)
|
|
174
|
+
|
|
175
|
+
# Or with the convenience method
|
|
176
|
+
client = PostmarkClient.emails(api_token: "different-token")
|
|
177
|
+
response = client.send_email(
|
|
178
|
+
from: "sender@example.com",
|
|
179
|
+
to: "recipient@example.com",
|
|
180
|
+
subject: "Hello",
|
|
181
|
+
text_body: "World"
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Error Handling
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
begin
|
|
189
|
+
response = PostmarkClient.deliver(email)
|
|
190
|
+
rescue PostmarkClient::ValidationError => e
|
|
191
|
+
# Email failed local validation before sending
|
|
192
|
+
puts "Validation error: #{e.message}"
|
|
193
|
+
rescue PostmarkClient::ApiError => e
|
|
194
|
+
# Postmark API returned an error
|
|
195
|
+
puts "API error #{e.error_code}: #{e.message}"
|
|
196
|
+
puts "Full response: #{e.response}"
|
|
197
|
+
rescue PostmarkClient::ConnectionError => e
|
|
198
|
+
# Network connectivity issues
|
|
199
|
+
puts "Connection error: #{e.message}"
|
|
200
|
+
rescue PostmarkClient::ConfigurationError => e
|
|
201
|
+
# Missing or invalid configuration
|
|
202
|
+
puts "Configuration error: #{e.message}"
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## API Reference
|
|
207
|
+
|
|
208
|
+
### PostmarkClient::Email
|
|
209
|
+
|
|
210
|
+
| Attribute | Type | Description |
|
|
211
|
+
|-----------|------|-------------|
|
|
212
|
+
| `from` | String | Sender email (required) |
|
|
213
|
+
| `to` | String/Array | Recipient email(s) (required) |
|
|
214
|
+
| `cc` | String/Array | CC recipient(s) |
|
|
215
|
+
| `bcc` | String/Array | BCC recipient(s) |
|
|
216
|
+
| `subject` | String | Email subject |
|
|
217
|
+
| `html_body` | String | HTML email body |
|
|
218
|
+
| `text_body` | String | Plain text body |
|
|
219
|
+
| `reply_to` | String | Reply-to address |
|
|
220
|
+
| `tag` | String | Email tag for categorization |
|
|
221
|
+
| `headers` | Array | Custom email headers |
|
|
222
|
+
| `track_opens` | Boolean | Enable open tracking |
|
|
223
|
+
| `track_links` | String | Link tracking ("None", "HtmlAndText", "HtmlOnly", "TextOnly") |
|
|
224
|
+
| `attachments` | Array | Email attachments |
|
|
225
|
+
| `metadata` | Hash | Custom metadata key-value pairs |
|
|
226
|
+
| `message_stream` | String | Message stream identifier |
|
|
227
|
+
|
|
228
|
+
### PostmarkClient::EmailResponse
|
|
229
|
+
|
|
230
|
+
| Method | Returns | Description |
|
|
231
|
+
|--------|---------|-------------|
|
|
232
|
+
| `success?` | Boolean | True if email was sent successfully |
|
|
233
|
+
| `error?` | Boolean | True if there was an error |
|
|
234
|
+
| `message_id` | String | Unique message identifier |
|
|
235
|
+
| `to` | String | Recipient address |
|
|
236
|
+
| `submitted_at` | Time | Timestamp when email was submitted |
|
|
237
|
+
| `error_code` | Integer | Postmark error code (0 = success) |
|
|
238
|
+
| `message` | String | Response message |
|
|
239
|
+
| `raw_response` | Hash | Full API response |
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# Install dependencies
|
|
247
|
+
bundle install
|
|
248
|
+
|
|
249
|
+
# Run tests
|
|
250
|
+
bundle exec rspec
|
|
251
|
+
|
|
252
|
+
# Generate documentation
|
|
253
|
+
bundle exec yard doc
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
# View documentation in browser
|
|
257
|
+
bundle exec yard server
|
|
258
|
+
# Then open http://localhost:8808
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/postmark_ruby_client.
|
|
264
|
+
|
|
265
|
+
1. Fork the repository
|
|
266
|
+
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
|
267
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
268
|
+
4. Push to the branch (`git push origin feature/my-new-feature`)
|
|
269
|
+
5. Create a new Pull Request
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
274
|
+
|
|
275
|
+
## Related Resources
|
|
276
|
+
|
|
277
|
+
- [Postmark API Documentation](https://postmarkapp.com/developer)
|
|
278
|
+
- [Postmark Email API Reference](https://postmarkapp.com/developer/api/email-api)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
task default: %i[spec]
|
|
9
|
+
|
|
10
|
+
namespace :doc do
|
|
11
|
+
desc "Generate YARD documentation"
|
|
12
|
+
task :yard do
|
|
13
|
+
sh "yard doc --output-dir doc/yard"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Open an IRB console with the gem loaded"
|
|
18
|
+
task :console do
|
|
19
|
+
require "irb"
|
|
20
|
+
require_relative "lib/postmark_client"
|
|
21
|
+
|
|
22
|
+
ARGV.clear
|
|
23
|
+
IRB.start
|
|
24
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module PostmarkClient
|
|
7
|
+
module Client
|
|
8
|
+
# Base client class for all Postmark API interactions.
|
|
9
|
+
# Provides common HTTP functionality using Faraday.
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a custom resource client
|
|
12
|
+
# class MyResource < PostmarkClient::Client::Base
|
|
13
|
+
# def fetch(id)
|
|
14
|
+
# get("/my-resource/#{id}")
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @abstract Subclass and implement specific API resource methods
|
|
19
|
+
class Base
|
|
20
|
+
# Postmark API base URL
|
|
21
|
+
API_BASE_URL = "https://api.postmarkapp.com"
|
|
22
|
+
|
|
23
|
+
# @return [String] the API token for authentication
|
|
24
|
+
attr_reader :api_token
|
|
25
|
+
|
|
26
|
+
# @return [Hash] additional options passed to the client
|
|
27
|
+
attr_reader :options
|
|
28
|
+
|
|
29
|
+
# Initialize a new API client
|
|
30
|
+
#
|
|
31
|
+
# @param api_token [String, nil] the Postmark server API token.
|
|
32
|
+
# Falls back to PostmarkClient.configuration.api_token if not provided.
|
|
33
|
+
# @param options [Hash] additional configuration options
|
|
34
|
+
# @option options [Integer] :timeout request timeout in seconds (default: 30)
|
|
35
|
+
# @option options [Integer] :open_timeout connection open timeout in seconds (default: 10)
|
|
36
|
+
# @option options [String] :base_url override the default API base URL
|
|
37
|
+
#
|
|
38
|
+
# @raise [PostmarkClient::ConfigurationError] if no API token is available
|
|
39
|
+
def initialize(api_token: nil, **options)
|
|
40
|
+
@api_token = api_token || PostmarkClient.configuration.api_token
|
|
41
|
+
@options = options
|
|
42
|
+
|
|
43
|
+
raise ConfigurationError, "API token is required" if @api_token.nil? || @api_token.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
# Perform a GET request to the Postmark API
|
|
49
|
+
#
|
|
50
|
+
# @param path [String] the API endpoint path
|
|
51
|
+
# @param params [Hash] query parameters
|
|
52
|
+
# @return [Hash] parsed JSON response
|
|
53
|
+
def get(path, params = {})
|
|
54
|
+
request(:get, path, params)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Perform a POST request to the Postmark API
|
|
58
|
+
#
|
|
59
|
+
# @param path [String] the API endpoint path
|
|
60
|
+
# @param body [Hash] request body
|
|
61
|
+
# @return [Hash] parsed JSON response
|
|
62
|
+
def post(path, body = {})
|
|
63
|
+
request(:post, path, body)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Perform a PUT request to the Postmark API
|
|
67
|
+
#
|
|
68
|
+
# @param path [String] the API endpoint path
|
|
69
|
+
# @param body [Hash] request body
|
|
70
|
+
# @return [Hash] parsed JSON response
|
|
71
|
+
def put(path, body = {})
|
|
72
|
+
request(:put, path, body)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Perform a DELETE request to the Postmark API
|
|
76
|
+
#
|
|
77
|
+
# @param path [String] the API endpoint path
|
|
78
|
+
# @param params [Hash] query parameters
|
|
79
|
+
# @return [Hash] parsed JSON response
|
|
80
|
+
def delete(path, params = {})
|
|
81
|
+
request(:delete, path, params)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Build and return a configured Faraday connection
|
|
87
|
+
#
|
|
88
|
+
# @return [Faraday::Connection] configured connection instance
|
|
89
|
+
def connection
|
|
90
|
+
@connection ||= Faraday.new(url: base_url) do |conn|
|
|
91
|
+
conn.request :json
|
|
92
|
+
conn.response :json, content_type: /\bjson$/
|
|
93
|
+
conn.response :raise_error
|
|
94
|
+
|
|
95
|
+
conn.headers["Accept"] = "application/json"
|
|
96
|
+
conn.headers["Content-Type"] = "application/json"
|
|
97
|
+
conn.headers["X-Postmark-Server-Token"] = api_token
|
|
98
|
+
|
|
99
|
+
conn.options.timeout = options.fetch(:timeout, 30)
|
|
100
|
+
conn.options.open_timeout = options.fetch(:open_timeout, 10)
|
|
101
|
+
|
|
102
|
+
conn.adapter Faraday.default_adapter
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get the base URL for API requests
|
|
107
|
+
#
|
|
108
|
+
# @return [String] the API base URL
|
|
109
|
+
def base_url
|
|
110
|
+
options.fetch(:base_url, API_BASE_URL)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Perform an HTTP request and handle the response
|
|
114
|
+
#
|
|
115
|
+
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
|
116
|
+
# @param path [String] the API endpoint path
|
|
117
|
+
# @param payload [Hash] request body or query parameters
|
|
118
|
+
# @return [Hash] parsed JSON response
|
|
119
|
+
#
|
|
120
|
+
# @raise [PostmarkClient::ApiError] on API error responses
|
|
121
|
+
# @raise [PostmarkClient::ConnectionError] on network errors
|
|
122
|
+
def request(method, path, payload = {})
|
|
123
|
+
response = case method
|
|
124
|
+
when :get, :delete
|
|
125
|
+
connection.public_send(method, path, payload)
|
|
126
|
+
when :post, :put
|
|
127
|
+
connection.public_send(method, path, payload)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
response.body
|
|
131
|
+
rescue Faraday::ClientError => e
|
|
132
|
+
handle_client_error(e)
|
|
133
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
134
|
+
raise ConnectionError, "Connection failed: #{e.message}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Handle Faraday client errors and raise appropriate exceptions
|
|
138
|
+
#
|
|
139
|
+
# @param error [Faraday::ClientError] the caught error
|
|
140
|
+
# @raise [PostmarkClient::ApiError] with details from the response
|
|
141
|
+
def handle_client_error(error)
|
|
142
|
+
body = parse_error_body(error.response&.dig(:body))
|
|
143
|
+
error_code = body["ErrorCode"] || error.response&.dig(:status)
|
|
144
|
+
message = body["Message"] || error.message
|
|
145
|
+
|
|
146
|
+
raise ApiError.new(message, error_code: error_code, response: body)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Parse error body which may be a string or hash
|
|
150
|
+
#
|
|
151
|
+
# @param body [String, Hash, nil] the response body
|
|
152
|
+
# @return [Hash] parsed body
|
|
153
|
+
def parse_error_body(body)
|
|
154
|
+
return {} if body.nil?
|
|
155
|
+
return body if body.is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
JSON.parse(body)
|
|
158
|
+
rescue JSON::ParserError
|
|
159
|
+
{}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostmarkClient
|
|
4
|
+
# Configuration class for PostmarkClient gem.
|
|
5
|
+
# Stores global settings that apply to all API clients.
|
|
6
|
+
#
|
|
7
|
+
# @example Configuring the gem in a Rails initializer
|
|
8
|
+
# # config/initializers/postmark_ruby_client.rb
|
|
9
|
+
# PostmarkClient.configure do |config|
|
|
10
|
+
# config.api_token = ENV["POSTMARK_API_TOKEN"]
|
|
11
|
+
# config.default_message_stream = "outbound"
|
|
12
|
+
# config.timeout = 60
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Accessing configuration
|
|
16
|
+
# PostmarkClient.configuration.api_token
|
|
17
|
+
class Configuration
|
|
18
|
+
# @return [String, nil] the Postmark server API token
|
|
19
|
+
attr_accessor :api_token
|
|
20
|
+
|
|
21
|
+
# @return [String] the default message stream for emails (default: "outbound")
|
|
22
|
+
attr_accessor :default_message_stream
|
|
23
|
+
|
|
24
|
+
# @return [Integer] request timeout in seconds (default: 30)
|
|
25
|
+
attr_accessor :timeout
|
|
26
|
+
|
|
27
|
+
# @return [Integer] connection open timeout in seconds (default: 10)
|
|
28
|
+
attr_accessor :open_timeout
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] whether to track email opens by default (default: false)
|
|
31
|
+
attr_accessor :track_opens
|
|
32
|
+
|
|
33
|
+
# @return [String] default link tracking setting (default: "None")
|
|
34
|
+
# Valid values: "None", "HtmlAndText", "HtmlOnly", "TextOnly"
|
|
35
|
+
attr_accessor :track_links
|
|
36
|
+
|
|
37
|
+
# Initialize configuration with default values
|
|
38
|
+
def initialize
|
|
39
|
+
@api_token = ENV.fetch("POSTMARK_API_TOKEN", nil)
|
|
40
|
+
@default_message_stream = "outbound"
|
|
41
|
+
@timeout = 30
|
|
42
|
+
@open_timeout = 10
|
|
43
|
+
@track_opens = false
|
|
44
|
+
@track_links = "None"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# @return [Configuration] the global configuration instance
|
|
50
|
+
attr_writer :configuration
|
|
51
|
+
|
|
52
|
+
# Get the current configuration, initializing if necessary
|
|
53
|
+
#
|
|
54
|
+
# @return [Configuration] the global configuration instance
|
|
55
|
+
def configuration
|
|
56
|
+
@configuration ||= Configuration.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Configure the gem using a block
|
|
60
|
+
#
|
|
61
|
+
# @yield [Configuration] the configuration instance
|
|
62
|
+
# @return [void]
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# PostmarkClient.configure do |config|
|
|
66
|
+
# config.api_token = "your-token"
|
|
67
|
+
# end
|
|
68
|
+
def configure
|
|
69
|
+
yield(configuration)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reset the configuration to defaults
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def reset_configuration!
|
|
76
|
+
@configuration = Configuration.new
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostmarkClient
|
|
4
|
+
# Base error class for all PostmarkClient errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when configuration is invalid or missing
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# raise ConfigurationError, "API token is required"
|
|
11
|
+
class ConfigurationError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when there are network connectivity issues
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# raise ConnectionError, "Connection timeout"
|
|
17
|
+
class ConnectionError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised when the Postmark API returns an error response
|
|
20
|
+
#
|
|
21
|
+
# @example Handling API errors
|
|
22
|
+
# begin
|
|
23
|
+
# client.send_email(email)
|
|
24
|
+
# rescue PostmarkClient::ApiError => e
|
|
25
|
+
# puts "Error #{e.error_code}: #{e.message}"
|
|
26
|
+
# puts "Response: #{e.response}"
|
|
27
|
+
# end
|
|
28
|
+
class ApiError < Error
|
|
29
|
+
# @return [Integer, String, nil] the Postmark error code
|
|
30
|
+
attr_reader :error_code
|
|
31
|
+
|
|
32
|
+
# @return [Hash, nil] the full error response body
|
|
33
|
+
attr_reader :response
|
|
34
|
+
|
|
35
|
+
# Initialize a new API error
|
|
36
|
+
#
|
|
37
|
+
# @param message [String] the error message
|
|
38
|
+
# @param error_code [Integer, String, nil] the Postmark error code
|
|
39
|
+
# @param response [Hash, nil] the full response body
|
|
40
|
+
def initialize(message, error_code: nil, response: nil)
|
|
41
|
+
@error_code = error_code
|
|
42
|
+
@response = response
|
|
43
|
+
super(message)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised when email validation fails before sending
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# raise ValidationError, "From address is required"
|
|
51
|
+
class ValidationError < Error; end
|
|
52
|
+
end
|