camelmailer 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/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +34 -0
- data/LICENSE +21 -0
- data/README.md +167 -0
- data/lib/camelmailer/bounces.rb +17 -0
- data/lib/camelmailer/client.rb +138 -0
- data/lib/camelmailer/dmarc.rb +23 -0
- data/lib/camelmailer/emails.rb +62 -0
- data/lib/camelmailer/errors.rb +56 -0
- data/lib/camelmailer/mailer.rb +134 -0
- data/lib/camelmailer/railtie.rb +14 -0
- data/lib/camelmailer/resource.rb +26 -0
- data/lib/camelmailer/stats.rb +18 -0
- data/lib/camelmailer/streams.rb +30 -0
- data/lib/camelmailer/templates.rb +35 -0
- data/lib/camelmailer/version.rb +5 -0
- data/lib/camelmailer.rb +58 -0
- data/sig/camelmailer.rbs +170 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f9d144156fc057f5dede175ed29f43b9c6587a33ae84b7b25c097a814c1537b7
|
|
4
|
+
data.tar.gz: 482b37a49af5b42f3a527788a59c1dfaeb115797ed985c202deb7841c8fd8058
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 865924a5a43a2087e6e73abfb70e5fbb677b7cf9d961aa893bca18eec7859320f7a879d97a4d22d16e37d1b1840f1490ea1436692a357aa74aa24302a4d29801
|
|
7
|
+
data.tar.gz: a14c759e870581dec25f9a8eb3bd1cdf2e24ba4e9a9989d9128193e45858ee1f51022dc5a7886e33f31f7d2c61d6c5b60766f42fd95a498c85e892282cfb15b3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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.1.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-07-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `CamelMailer::Client` on stdlib net/http — zero runtime dependencies.
|
|
15
|
+
- Global configuration via `CamelMailer.configure` (api_key, base_url for self-hosted instances).
|
|
16
|
+
- Messaging resources: `Emails` (send, send_batch, send_with_template, send_with_template_batch, get, list, deliveries, opens, clicks, raw), `Templates` (list, create, get, update, archive, render), `Streams` (list, create, get, update, archive), `Stats` (get, deliveries), `Bounces` (list, get), `Dmarc` (summary, reports, report).
|
|
17
|
+
- Typed error hierarchy with `code`, `message` and `status_code` (`UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ValidationError`, `ParameterMissingError`, `ServerError`, `ConnectionError`).
|
|
18
|
+
- Rails integration: `:camelmailer` ActionMailer delivery method with full MIME mapping (from/to/cc/bcc/reply_to, html+text parts, attachments, custom headers, tag/stream/metadata pseudo-headers).
|
|
19
|
+
- RBS type signatures.
|
|
20
|
+
|
|
21
|
+
[Unreleased]: https://github.com/camelmailer/camelmailer-ruby/compare/v0.1.0...HEAD
|
|
22
|
+
[0.1.0]: https://github.com/camelmailer/camelmailer-ruby/releases/tag/v0.1.0
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
git clone https://github.com/camelmailer/camelmailer-ruby
|
|
7
|
+
cd camelmailer-ruby
|
|
8
|
+
bundle install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Ruby >= 3.1.
|
|
12
|
+
|
|
13
|
+
## Tests & lint
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bundle exec rspec # unit tests (WebMock, no network)
|
|
17
|
+
bundle exec rubocop # lint
|
|
18
|
+
bundle exec rake # both
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Integration tests run against a real instance and are skipped without credentials:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
CAMELMAILER_API_KEY=cm_... CAMELMAILER_BASE_URL=https://mail.example.com \
|
|
25
|
+
CAMELMAILER_FROM=billing@acme.com CAMELMAILER_TO=inbox@example.com \
|
|
26
|
+
bundle exec rspec spec/integration
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Conventions
|
|
30
|
+
|
|
31
|
+
- Test-first: every resource method and error path has a spec against a mocked HTTP layer.
|
|
32
|
+
- No runtime dependencies — stdlib `net/http` only.
|
|
33
|
+
- Follow the existing envelope handling in `CamelMailer::Client`; new endpoints go on the matching resource class plus an `expose` entry for the class-level API.
|
|
34
|
+
- Update `CHANGELOG.md` (Keep a Changelog) with user-visible changes.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Camelmailer contributors
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# camelmailer-ruby
|
|
2
|
+
|
|
3
|
+
[](https://github.com/camelmailer/camelmailer-ruby/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/camelmailer)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
The Ruby and Rails SDK for [Camelmailer](https://camelmailer.com) — open-source transactional email. No runtime dependencies, Ruby >= 3.1.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
gem install camelmailer
|
|
13
|
+
# or in your Gemfile
|
|
14
|
+
gem "camelmailer"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "camelmailer"
|
|
21
|
+
|
|
22
|
+
CamelMailer.configure { |c| c.api_key = "cm_..." }
|
|
23
|
+
|
|
24
|
+
CamelMailer::Emails.send(
|
|
25
|
+
from: "billing@acme.com",
|
|
26
|
+
to: ["ada@example.com"],
|
|
27
|
+
subject: "Your receipt",
|
|
28
|
+
html_body: "<h1>Thanks!</h1>"
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or without global state:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
client = CamelMailer::Client.new(api_key: "cm_...")
|
|
36
|
+
client.emails.send(from: "billing@acme.com", to: ["ada@example.com"], subject: "Hi", text_body: "Hello")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Every resource is available both ways: `CamelMailer::Emails.send(...)` uses the global configuration, `client.emails.send(...)` uses an explicit client.
|
|
40
|
+
|
|
41
|
+
## Self-hosted
|
|
42
|
+
|
|
43
|
+
The base URL defaults to the Camelmailer cloud (`https://app.camelmailer.com`). Point it at your own instance:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
CamelMailer.configure do |c|
|
|
47
|
+
c.api_key = "cm_..."
|
|
48
|
+
c.base_url = "https://mail.example.com"
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Rails
|
|
53
|
+
|
|
54
|
+
The gem registers the `:camelmailer` ActionMailer delivery method automatically:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# config/environments/production.rb
|
|
58
|
+
config.action_mailer.delivery_method = :camelmailer
|
|
59
|
+
config.action_mailer.camelmailer_settings = {
|
|
60
|
+
api_key: Rails.application.credentials.dig(:camelmailer, :api_key),
|
|
61
|
+
base_url: "https://mail.example.com" # optional, self-hosted only
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then use ActionMailer as usual — including attachments, multipart bodies and custom headers:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class ReceiptMailer < ApplicationMailer
|
|
69
|
+
def receipt
|
|
70
|
+
attachments["invoice.pdf"] = File.read("invoice.pdf")
|
|
71
|
+
mail(
|
|
72
|
+
from: "billing@acme.com",
|
|
73
|
+
to: params[:to],
|
|
74
|
+
subject: "Your receipt",
|
|
75
|
+
tag: "receipt", # CamelMailer tag (filtering & stats)
|
|
76
|
+
stream: "transactional" # optional message stream
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Resources
|
|
83
|
+
|
|
84
|
+
### Emails
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
CamelMailer::Emails.send(from:, to:, subject:, html_body:, text_body:, cc:, bcc:, reply_to:, headers:, attachments:, tag:, metadata:, stream:)
|
|
88
|
+
CamelMailer::Emails.send_batch([{ from:, to:, ... }, ...])
|
|
89
|
+
CamelMailer::Emails.send_with_template(from:, to:, template: "welcome", template_model: { name: "Ada" })
|
|
90
|
+
CamelMailer::Emails.send_with_template_batch([...])
|
|
91
|
+
CamelMailer::Emails.get(42)
|
|
92
|
+
CamelMailer::Emails.list(scope: "outgoing", status: "Sent", tag: "receipt", query: "ada", page: 1, per_page: 50)
|
|
93
|
+
CamelMailer::Emails.deliveries(42) # delivery attempts
|
|
94
|
+
CamelMailer::Emails.opens(42) # open events
|
|
95
|
+
CamelMailer::Emails.clicks(42) # click events
|
|
96
|
+
CamelMailer::Emails.raw(42) # raw RFC 5322 source
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Attachments are `{ name:, content_type:, data_base64: }`; addresses are either `"a@b.com"` or `{ email: "a@b.com", name: "Ada" }`.
|
|
100
|
+
|
|
101
|
+
### Templates
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
CamelMailer::Templates.list
|
|
105
|
+
CamelMailer::Templates.create(name: "Welcome", subject: "Hi {{ name }}", html_body: "<p>Hi {{ name }}</p>")
|
|
106
|
+
CamelMailer::Templates.get("welcome")
|
|
107
|
+
CamelMailer::Templates.update("welcome", subject: "Hello {{ name }}")
|
|
108
|
+
CamelMailer::Templates.archive("welcome")
|
|
109
|
+
CamelMailer::Templates.render("welcome", { name: "Ada" }) # preview without sending
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Streams
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
CamelMailer::Streams.list
|
|
116
|
+
CamelMailer::Streams.create(name: "Broadcasts", stream_type: "broadcast")
|
|
117
|
+
CamelMailer::Streams.get("broadcasts")
|
|
118
|
+
CamelMailer::Streams.update("broadcasts", name: "News")
|
|
119
|
+
CamelMailer::Streams.archive("broadcasts")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Stats & bounces
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
CamelMailer::Stats.get(from: "2026-01-01T00:00:00Z", to: "2026-02-01T00:00:00Z")
|
|
126
|
+
CamelMailer::Stats.deliveries
|
|
127
|
+
CamelMailer::Bounces.list(page: 1, per_page: 50)
|
|
128
|
+
CamelMailer::Bounces.get(42)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### DMARC
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
CamelMailer::Dmarc.summary(domain: "acme.com")
|
|
135
|
+
CamelMailer::Dmarc.reports(domain: "acme.com", page: 1)
|
|
136
|
+
CamelMailer::Dmarc.report(3)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error handling
|
|
140
|
+
|
|
141
|
+
All API errors raise typed exceptions with `code`, `message` and `status_code`:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
begin
|
|
145
|
+
CamelMailer::Emails.send(from: "x@unverified.test", to: ["a@b.com"])
|
|
146
|
+
rescue CamelMailer::ValidationError => e
|
|
147
|
+
e.code # => "ValidationError"
|
|
148
|
+
e.message # => "from domain not verified"
|
|
149
|
+
e.status_code # => 422
|
|
150
|
+
rescue CamelMailer::UnauthorizedError
|
|
151
|
+
# bad API key
|
|
152
|
+
rescue CamelMailer::ConnectionError
|
|
153
|
+
# network problem
|
|
154
|
+
rescue CamelMailer::Error => e
|
|
155
|
+
# everything above inherits from this
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Hierarchy: `Error` → `ConnectionError`, `APIError` → `UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ValidationError`, `ParameterMissingError`, `ServerError`.
|
|
160
|
+
|
|
161
|
+
## Docs
|
|
162
|
+
|
|
163
|
+
Full API reference: [camelmailer.com/docs](https://camelmailer.com/docs)
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Read bounce messages (/api/v2/server/bounces...).
|
|
5
|
+
class Bounces < Resource
|
|
6
|
+
expose :list, :get
|
|
7
|
+
|
|
8
|
+
# Lists bounces. Filters: page:, per_page: (max 100).
|
|
9
|
+
def list(**filters)
|
|
10
|
+
client.get("bounces", filters)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(id)
|
|
14
|
+
client.get("bounces/#{id}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module CamelMailer
|
|
9
|
+
# HTTP client for the CamelMailer messaging API (/api/v2/server).
|
|
10
|
+
#
|
|
11
|
+
# client = CamelMailer::Client.new(api_key: "cm_...")
|
|
12
|
+
# client.emails.send(from: "a@acme.com", to: ["b@example.com"], subject: "Hi", text_body: "Hello")
|
|
13
|
+
#
|
|
14
|
+
# Built on net/http — the gem has no runtime dependencies.
|
|
15
|
+
class Client
|
|
16
|
+
API_PREFIX = "/api/v2/server"
|
|
17
|
+
|
|
18
|
+
NETWORK_ERRORS = [
|
|
19
|
+
SocketError, EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
|
20
|
+
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::EPIPE,
|
|
21
|
+
Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :api_key, :base_url, :open_timeout, :read_timeout
|
|
25
|
+
|
|
26
|
+
def initialize(api_key:, base_url: nil, open_timeout: 10, read_timeout: 30)
|
|
27
|
+
if api_key.nil? || api_key.to_s.strip.empty?
|
|
28
|
+
raise Error, "No CamelMailer API key provided — set CamelMailer.api_key or pass api_key:"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@api_key = api_key
|
|
32
|
+
@base_url = (base_url || DEFAULT_BASE_URL).to_s.chomp("/")
|
|
33
|
+
@open_timeout = open_timeout
|
|
34
|
+
@read_timeout = read_timeout
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def emails
|
|
38
|
+
@emails ||= Emails.new(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def templates
|
|
42
|
+
@templates ||= Templates.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def streams
|
|
46
|
+
@streams ||= Streams.new(self)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stats
|
|
50
|
+
@stats ||= Stats.new(self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def bounces
|
|
54
|
+
@bounces ||= Bounces.new(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def dmarc
|
|
58
|
+
@dmarc ||= Dmarc.new(self)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get(path, query = nil)
|
|
62
|
+
perform(Net::HTTP::Get.new(build_uri(path, query)))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def post(path, body = nil)
|
|
66
|
+
req = Net::HTTP::Post.new(build_uri(path))
|
|
67
|
+
attach_body(req, body)
|
|
68
|
+
perform(req)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def patch(path, body = nil)
|
|
72
|
+
req = Net::HTTP::Patch.new(build_uri(path))
|
|
73
|
+
attach_body(req, body)
|
|
74
|
+
perform(req)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def build_uri(path, query = nil)
|
|
80
|
+
uri = URI.parse("#{base_url}#{API_PREFIX}/#{path}")
|
|
81
|
+
if query
|
|
82
|
+
params = query.compact
|
|
83
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
84
|
+
end
|
|
85
|
+
uri
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def attach_body(req, body)
|
|
89
|
+
return unless body
|
|
90
|
+
|
|
91
|
+
req["Content-Type"] = "application/json"
|
|
92
|
+
req.body = JSON.generate(body)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def perform(req)
|
|
96
|
+
req["X-Server-API-Key"] = api_key
|
|
97
|
+
req["Accept"] = "application/json"
|
|
98
|
+
req["User-Agent"] = "camelmailer-ruby/#{VERSION}"
|
|
99
|
+
|
|
100
|
+
handle(transport(req.uri).request(req))
|
|
101
|
+
rescue *NETWORK_ERRORS => e
|
|
102
|
+
raise ConnectionError, "Could not reach the CamelMailer API: #{e.class}: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def transport(uri)
|
|
106
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
107
|
+
http.use_ssl = uri.scheme == "https"
|
|
108
|
+
http.open_timeout = open_timeout
|
|
109
|
+
http.read_timeout = read_timeout
|
|
110
|
+
http
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle(res)
|
|
114
|
+
status = res.code.to_i
|
|
115
|
+
body = parse_body(res.body)
|
|
116
|
+
|
|
117
|
+
case body
|
|
118
|
+
in { status: "success" }
|
|
119
|
+
body[:data] || {}
|
|
120
|
+
in { status: "error" }
|
|
121
|
+
error = body[:error] || {}
|
|
122
|
+
raise APIError.for(code: error[:code], message: error[:message], status_code: status)
|
|
123
|
+
else
|
|
124
|
+
return body.is_a?(Hash) ? body : {} if (200..299).cover?(status)
|
|
125
|
+
|
|
126
|
+
raise ServerError.new("Unexpected HTTP #{status} response from the CamelMailer API", status_code: status)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_body(raw)
|
|
131
|
+
return nil if raw.nil? || raw.empty?
|
|
132
|
+
|
|
133
|
+
JSON.parse(raw, symbolize_names: true)
|
|
134
|
+
rescue JSON::ParserError
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# DMARC compliance summary and aggregate reports (/api/v2/server/dmarc...).
|
|
5
|
+
class Dmarc < Resource
|
|
6
|
+
expose :summary, :reports, :report
|
|
7
|
+
|
|
8
|
+
# Compliance summary. Filters: domain:, from:, to: (ISO 8601).
|
|
9
|
+
def summary(domain: nil, from: nil, to: nil)
|
|
10
|
+
client.get("dmarc/summary", { domain: domain, from: from, to: to })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Stored aggregate reports, newest report range first.
|
|
14
|
+
def reports(domain: nil, from: nil, to: nil, page: nil, per_page: nil)
|
|
15
|
+
client.get("dmarc/reports", { domain: domain, from: from, to: to, page: page, per_page: per_page })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# One report with its records.
|
|
19
|
+
def report(id)
|
|
20
|
+
client.get("dmarc/reports/#{id}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Send and read messages (POST/GET /api/v2/server/messages...).
|
|
5
|
+
class Emails < Resource
|
|
6
|
+
expose :send, :send_batch, :send_with_template, :send_with_template_batch,
|
|
7
|
+
:get, :list, :deliveries, :opens, :clicks, :raw
|
|
8
|
+
|
|
9
|
+
# Sends one message. Required: from:, to:. See the API docs for the
|
|
10
|
+
# full SendRequest shape (html_body, text_body, cc, bcc, reply_to,
|
|
11
|
+
# headers, attachments, tag, metadata, stream).
|
|
12
|
+
def send(params)
|
|
13
|
+
client.post("messages", params)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Sends a batch of SendRequest hashes; returns one result per entry.
|
|
17
|
+
def send_batch(messages)
|
|
18
|
+
client.post("messages/batch", { messages: messages })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Renders a stored template against template_model:, then sends.
|
|
22
|
+
def send_with_template(params)
|
|
23
|
+
client.post("messages/with_template", params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Batch variant of #send_with_template.
|
|
27
|
+
def send_with_template_batch(messages)
|
|
28
|
+
client.post("messages/with_template/batch", { messages: messages })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Shows one message by id.
|
|
32
|
+
def get(id)
|
|
33
|
+
client.get("messages/#{id}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Lists messages. Filters: scope:, status:, tag:, query:, stream:,
|
|
37
|
+
# page:, per_page: (max 100).
|
|
38
|
+
def list(**filters)
|
|
39
|
+
client.get("messages", filters)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Delivery attempts of a message.
|
|
43
|
+
def deliveries(id)
|
|
44
|
+
client.get("messages/#{id}/deliveries")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Open events of a message.
|
|
48
|
+
def opens(id)
|
|
49
|
+
client.get("messages/#{id}/opens")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Click events of a message.
|
|
53
|
+
def clicks(id)
|
|
54
|
+
client.get("messages/#{id}/clicks")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Raw RFC 5322 source of a message.
|
|
58
|
+
def raw(id)
|
|
59
|
+
client.get("messages/#{id}/raw")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Base error for everything raised by this gem.
|
|
5
|
+
#
|
|
6
|
+
# Exposes the stable API error +code+ (e.g. "ValidationError") and the
|
|
7
|
+
# HTTP +status_code+ of the response, when available.
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
attr_reader :code, :status_code
|
|
10
|
+
|
|
11
|
+
def initialize(message = nil, code: nil, status_code: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@code = code
|
|
14
|
+
@status_code = status_code
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Raised when the API could not be reached at all (DNS, TCP, TLS, timeouts).
|
|
19
|
+
class ConnectionError < Error; end
|
|
20
|
+
|
|
21
|
+
# Raised for any error envelope returned by the API. Specific stable
|
|
22
|
+
# error codes map to subclasses; unknown codes raise APIError itself.
|
|
23
|
+
class APIError < Error
|
|
24
|
+
# Builds the most specific error class for an error envelope.
|
|
25
|
+
def self.for(code:, message:, status_code: nil)
|
|
26
|
+
klass = ERROR_CODE_CLASSES.fetch(code, APIError)
|
|
27
|
+
klass.new(message || code || "CamelMailer API error", code: code, status_code: status_code)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 401 — missing or invalid API key.
|
|
32
|
+
class UnauthorizedError < APIError; end
|
|
33
|
+
|
|
34
|
+
# 403 — the key is valid but not allowed to do this.
|
|
35
|
+
class ForbiddenError < APIError; end
|
|
36
|
+
|
|
37
|
+
# 404 — no such resource.
|
|
38
|
+
class NotFoundError < APIError; end
|
|
39
|
+
|
|
40
|
+
# 422 — the request was understood but invalid.
|
|
41
|
+
class ValidationError < APIError; end
|
|
42
|
+
|
|
43
|
+
# 400 — a required parameter is missing.
|
|
44
|
+
class ParameterMissingError < APIError; end
|
|
45
|
+
|
|
46
|
+
# Unexpected non-envelope response (e.g. a 5xx from a proxy).
|
|
47
|
+
class ServerError < APIError; end
|
|
48
|
+
|
|
49
|
+
APIError::ERROR_CODE_CLASSES = {
|
|
50
|
+
"Unauthorized" => UnauthorizedError,
|
|
51
|
+
"Forbidden" => ForbiddenError,
|
|
52
|
+
"NotFound" => NotFoundError,
|
|
53
|
+
"ValidationError" => ValidationError,
|
|
54
|
+
"ParameterMissing" => ParameterMissingError
|
|
55
|
+
}.freeze
|
|
56
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "camelmailer"
|
|
4
|
+
|
|
5
|
+
module CamelMailer
|
|
6
|
+
# ActionMailer delivery method (:camelmailer).
|
|
7
|
+
#
|
|
8
|
+
# config.action_mailer.delivery_method = :camelmailer
|
|
9
|
+
# config.action_mailer.camelmailer_settings = {
|
|
10
|
+
# api_key: Rails.application.credentials.dig(:camelmailer, :api_key),
|
|
11
|
+
# base_url: "https://mail.example.com" # optional, for self-hosted
|
|
12
|
+
# }
|
|
13
|
+
#
|
|
14
|
+
# Falls back to the global CamelMailer.configure settings when the
|
|
15
|
+
# delivery-method settings do not provide api_key/base_url.
|
|
16
|
+
#
|
|
17
|
+
# Special mail headers `tag`, `stream` and `metadata` are lifted out of
|
|
18
|
+
# the MIME headers and sent as first-class API fields:
|
|
19
|
+
#
|
|
20
|
+
# mail(to: ..., subject: ..., tag: "receipt", stream: "transactional")
|
|
21
|
+
class Mailer
|
|
22
|
+
# MIME headers that are structural or sent as dedicated API fields —
|
|
23
|
+
# everything else is forwarded via the +headers+ API parameter.
|
|
24
|
+
IGNORED_HEADERS = %w[
|
|
25
|
+
from to cc bcc reply-to subject date message-id mime-version
|
|
26
|
+
content-type content-transfer-encoding return-path received
|
|
27
|
+
tag stream metadata headers
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_accessor :settings
|
|
31
|
+
|
|
32
|
+
def initialize(settings = {})
|
|
33
|
+
@settings = settings || {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Delivers a Mail::Message through the CamelMailer API and assigns
|
|
37
|
+
# the resulting message id back onto the mail object.
|
|
38
|
+
def deliver!(mail)
|
|
39
|
+
data = Emails.new(client).send(build_params(mail))
|
|
40
|
+
mail.message_id = data[:message_id].to_s if data.is_a?(Hash) && data[:message_id]
|
|
41
|
+
data
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Maps a Mail::Message onto a CamelMailer SendRequest hash.
|
|
45
|
+
def build_params(mail)
|
|
46
|
+
params = {
|
|
47
|
+
from: single_address(mail[:from]),
|
|
48
|
+
to: address_list(mail[:to]),
|
|
49
|
+
cc: address_list(mail[:cc]),
|
|
50
|
+
bcc: address_list(mail[:bcc]),
|
|
51
|
+
reply_to: address_list(mail[:reply_to]),
|
|
52
|
+
subject: mail.subject
|
|
53
|
+
}
|
|
54
|
+
params.merge!(contents(mail))
|
|
55
|
+
params.merge!(pseudo_headers(mail))
|
|
56
|
+
headers = custom_headers(mail)
|
|
57
|
+
params[:headers] = headers unless headers.empty?
|
|
58
|
+
attachments = build_attachments(mail)
|
|
59
|
+
params[:attachments] = attachments unless attachments.empty?
|
|
60
|
+
params.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def client
|
|
66
|
+
Client.new(
|
|
67
|
+
api_key: settings[:api_key] || CamelMailer.api_key,
|
|
68
|
+
base_url: settings[:base_url] || CamelMailer.base_url
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def single_address(field)
|
|
73
|
+
address_list(field)&.first
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def address_list(field)
|
|
77
|
+
return nil unless field
|
|
78
|
+
|
|
79
|
+
field.addrs.map do |addr|
|
|
80
|
+
if addr.display_name
|
|
81
|
+
{ email: addr.address, name: addr.display_name }
|
|
82
|
+
else
|
|
83
|
+
addr.address
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def contents(mail)
|
|
89
|
+
case mail.mime_type
|
|
90
|
+
when "text/html"
|
|
91
|
+
{ html_body: mail.body.decoded }
|
|
92
|
+
when "multipart/alternative", "multipart/mixed", "multipart/related"
|
|
93
|
+
multipart_contents(mail)
|
|
94
|
+
else
|
|
95
|
+
{ text_body: mail.body.decoded }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def multipart_contents(mail)
|
|
100
|
+
params = {}
|
|
101
|
+
params[:text_body] = mail.text_part.decoded if mail.text_part
|
|
102
|
+
params[:html_body] = mail.html_part.decoded if mail.html_part
|
|
103
|
+
params
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Lifts the tag/stream/metadata pseudo-headers into API fields.
|
|
107
|
+
def pseudo_headers(mail)
|
|
108
|
+
params = {}
|
|
109
|
+
params[:tag] = mail["tag"].unparsed_value if mail["tag"]
|
|
110
|
+
params[:stream] = mail["stream"].unparsed_value if mail["stream"]
|
|
111
|
+
params[:metadata] = mail["metadata"].unparsed_value if mail["metadata"]
|
|
112
|
+
params
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def custom_headers(mail)
|
|
116
|
+
mail.header_fields.each_with_object({}) do |field, headers|
|
|
117
|
+
next if IGNORED_HEADERS.include?(field.name.downcase)
|
|
118
|
+
|
|
119
|
+
value = field.unparsed_value
|
|
120
|
+
headers[field.name] = value.to_s unless value.nil?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_attachments(mail)
|
|
125
|
+
mail.attachments.map do |part|
|
|
126
|
+
{
|
|
127
|
+
name: part.filename,
|
|
128
|
+
content_type: part.mime_type,
|
|
129
|
+
data_base64: [part.body.decoded].pack("m0")
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "camelmailer"
|
|
4
|
+
require "camelmailer/mailer"
|
|
5
|
+
|
|
6
|
+
module CamelMailer
|
|
7
|
+
# Registers the :camelmailer ActionMailer delivery method in Rails apps.
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
10
|
+
add_delivery_method :camelmailer, CamelMailer::Mailer
|
|
11
|
+
ActiveSupport.run_load_hooks(:camelmailer_mailer, CamelMailer::Mailer)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Base class for API resources.
|
|
5
|
+
#
|
|
6
|
+
# Resources are used through a client instance (client.emails.send ...)
|
|
7
|
+
# or through class-level convenience methods (CamelMailer::Emails.send ...)
|
|
8
|
+
# that use the globally configured client.
|
|
9
|
+
class Resource
|
|
10
|
+
# Defines class-level methods that delegate to an instance built
|
|
11
|
+
# from the global configuration (CamelMailer.configure).
|
|
12
|
+
def self.expose(*names)
|
|
13
|
+
names.each do |name|
|
|
14
|
+
define_singleton_method(name) do |*args, **kwargs|
|
|
15
|
+
new.public_send(name, *args, **kwargs)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :client
|
|
21
|
+
|
|
22
|
+
def initialize(client = CamelMailer.client)
|
|
23
|
+
@client = client
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Message counters and delivery statistics (/api/v2/server/stats...).
|
|
5
|
+
class Stats < Resource
|
|
6
|
+
expose :get, :deliveries
|
|
7
|
+
|
|
8
|
+
# Message counters, optionally windowed with from:/to: (ISO 8601).
|
|
9
|
+
def get(from: nil, to: nil)
|
|
10
|
+
client.get("stats", { from: from, to: to })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Delivery/queue statistics.
|
|
14
|
+
def deliveries
|
|
15
|
+
client.get("stats/deliveries")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Manage message streams (/api/v2/server/streams...).
|
|
5
|
+
class Streams < Resource
|
|
6
|
+
expose :list, :create, :get, :update, :archive
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
client.get("streams")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Creates a stream. Required: name:. Optional: stream_type:
|
|
13
|
+
# ("transactional" or "broadcast").
|
|
14
|
+
def create(params)
|
|
15
|
+
client.post("streams", params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(permalink)
|
|
19
|
+
client.get("streams/#{permalink}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update(permalink, params)
|
|
23
|
+
client.patch("streams/#{permalink}", params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def archive(permalink)
|
|
27
|
+
client.post("streams/#{permalink}/archive")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CamelMailer
|
|
4
|
+
# Manage stored message templates (/api/v2/server/templates...).
|
|
5
|
+
class Templates < Resource
|
|
6
|
+
expose :list, :create, :get, :update, :archive, :render
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
client.get("templates")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Creates a template. Required: name:. Optional: subject:, html_body:,
|
|
13
|
+
# text_body: — all may contain {{ variables }}.
|
|
14
|
+
def create(params)
|
|
15
|
+
client.post("templates", params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(permalink)
|
|
19
|
+
client.get("templates/#{permalink}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update(permalink, params)
|
|
23
|
+
client.patch("templates/#{permalink}", params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def archive(permalink)
|
|
27
|
+
client.post("templates/#{permalink}/archive")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Renders the template against a model without sending anything.
|
|
31
|
+
def render(permalink, template_model = {})
|
|
32
|
+
client.post("templates/#{permalink}/render", { template_model: template_model })
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/camelmailer.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "camelmailer/version"
|
|
4
|
+
require "camelmailer/errors"
|
|
5
|
+
require "camelmailer/client"
|
|
6
|
+
require "camelmailer/resource"
|
|
7
|
+
require "camelmailer/emails"
|
|
8
|
+
require "camelmailer/templates"
|
|
9
|
+
require "camelmailer/streams"
|
|
10
|
+
require "camelmailer/stats"
|
|
11
|
+
require "camelmailer/bounces"
|
|
12
|
+
require "camelmailer/dmarc"
|
|
13
|
+
|
|
14
|
+
# The Ruby SDK for CamelMailer — https://camelmailer.com
|
|
15
|
+
#
|
|
16
|
+
# CamelMailer.configure do |config|
|
|
17
|
+
# config.api_key = "cm_..."
|
|
18
|
+
# config.base_url = "https://mail.example.com" # optional, for self-hosted
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# CamelMailer::Emails.send(from: "a@acme.com", to: ["b@example.com"],
|
|
22
|
+
# subject: "Hello", text_body: "Hi!")
|
|
23
|
+
#
|
|
24
|
+
# Or with an explicit client (no global state):
|
|
25
|
+
#
|
|
26
|
+
# client = CamelMailer::Client.new(api_key: "cm_...")
|
|
27
|
+
# client.emails.send(...)
|
|
28
|
+
module CamelMailer
|
|
29
|
+
DEFAULT_BASE_URL = "https://app.camelmailer.com"
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
attr_accessor :api_key
|
|
33
|
+
attr_writer :base_url
|
|
34
|
+
|
|
35
|
+
def base_url
|
|
36
|
+
@base_url || DEFAULT_BASE_URL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def configure
|
|
40
|
+
yield self if block_given?
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
alias config configure
|
|
44
|
+
|
|
45
|
+
# A client built from the global configuration.
|
|
46
|
+
def client
|
|
47
|
+
Client.new(api_key: api_key, base_url: base_url)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Clears the global configuration (used in tests).
|
|
51
|
+
def reset!
|
|
52
|
+
@api_key = nil
|
|
53
|
+
@base_url = nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
require "camelmailer/railtie" if defined?(Rails::Railtie)
|
data/sig/camelmailer.rbs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
module CamelMailer
|
|
2
|
+
VERSION: String
|
|
3
|
+
DEFAULT_BASE_URL: String
|
|
4
|
+
|
|
5
|
+
type payload = Hash[Symbol | String, untyped]
|
|
6
|
+
type response = Hash[Symbol, untyped]
|
|
7
|
+
|
|
8
|
+
def self.api_key: () -> String?
|
|
9
|
+
def self.api_key=: (String?) -> String?
|
|
10
|
+
def self.base_url: () -> String
|
|
11
|
+
def self.base_url=: (String?) -> String?
|
|
12
|
+
def self.configure: () { (module) -> void } -> module
|
|
13
|
+
def self.config: () { (module) -> void } -> module
|
|
14
|
+
def self.client: () -> Client
|
|
15
|
+
def self.reset!: () -> void
|
|
16
|
+
|
|
17
|
+
class Error < StandardError
|
|
18
|
+
attr_reader code: String?
|
|
19
|
+
attr_reader status_code: Integer?
|
|
20
|
+
|
|
21
|
+
def initialize: (?String? message, ?code: String?, ?status_code: Integer?) -> void
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ConnectionError < Error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class APIError < Error
|
|
28
|
+
ERROR_CODE_CLASSES: Hash[String, singleton(APIError)]
|
|
29
|
+
|
|
30
|
+
def self.for: (code: String?, message: String?, ?status_code: Integer?) -> APIError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class UnauthorizedError < APIError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ForbiddenError < APIError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class NotFoundError < APIError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ValidationError < APIError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class ParameterMissingError < APIError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class ServerError < APIError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Client
|
|
52
|
+
API_PREFIX: String
|
|
53
|
+
NETWORK_ERRORS: Array[singleton(Exception)]
|
|
54
|
+
|
|
55
|
+
attr_reader api_key: String
|
|
56
|
+
attr_reader base_url: String
|
|
57
|
+
attr_reader open_timeout: Integer
|
|
58
|
+
attr_reader read_timeout: Integer
|
|
59
|
+
|
|
60
|
+
def initialize: (api_key: String?, ?base_url: String?, ?open_timeout: Integer, ?read_timeout: Integer) -> void
|
|
61
|
+
|
|
62
|
+
def emails: () -> Emails
|
|
63
|
+
def templates: () -> Templates
|
|
64
|
+
def streams: () -> Streams
|
|
65
|
+
def stats: () -> Stats
|
|
66
|
+
def bounces: () -> Bounces
|
|
67
|
+
def dmarc: () -> Dmarc
|
|
68
|
+
|
|
69
|
+
def get: (String path, ?payload? query) -> response
|
|
70
|
+
def post: (String path, ?payload? body) -> response
|
|
71
|
+
def patch: (String path, ?payload? body) -> response
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Resource
|
|
75
|
+
attr_reader client: Client
|
|
76
|
+
|
|
77
|
+
def self.expose: (*Symbol names) -> void
|
|
78
|
+
def initialize: (?Client client) -> void
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class Emails < Resource
|
|
82
|
+
def send: (payload params) -> response
|
|
83
|
+
def send_batch: (Array[payload] messages) -> response
|
|
84
|
+
def send_with_template: (payload params) -> response
|
|
85
|
+
def send_with_template_batch: (Array[payload] messages) -> response
|
|
86
|
+
def get: (Integer | String id) -> response
|
|
87
|
+
def list: (**untyped filters) -> response
|
|
88
|
+
def deliveries: (Integer | String id) -> response
|
|
89
|
+
def opens: (Integer | String id) -> response
|
|
90
|
+
def clicks: (Integer | String id) -> response
|
|
91
|
+
def raw: (Integer | String id) -> response
|
|
92
|
+
|
|
93
|
+
def self.send: (payload params) -> response
|
|
94
|
+
def self.send_batch: (Array[payload] messages) -> response
|
|
95
|
+
def self.send_with_template: (payload params) -> response
|
|
96
|
+
def self.send_with_template_batch: (Array[payload] messages) -> response
|
|
97
|
+
def self.get: (Integer | String id) -> response
|
|
98
|
+
def self.list: (**untyped filters) -> response
|
|
99
|
+
def self.deliveries: (Integer | String id) -> response
|
|
100
|
+
def self.opens: (Integer | String id) -> response
|
|
101
|
+
def self.clicks: (Integer | String id) -> response
|
|
102
|
+
def self.raw: (Integer | String id) -> response
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class Templates < Resource
|
|
106
|
+
def list: () -> response
|
|
107
|
+
def create: (payload params) -> response
|
|
108
|
+
def get: (String permalink) -> response
|
|
109
|
+
def update: (String permalink, payload params) -> response
|
|
110
|
+
def archive: (String permalink) -> response
|
|
111
|
+
def render: (String permalink, ?payload template_model) -> response
|
|
112
|
+
|
|
113
|
+
def self.list: () -> response
|
|
114
|
+
def self.create: (payload params) -> response
|
|
115
|
+
def self.get: (String permalink) -> response
|
|
116
|
+
def self.update: (String permalink, payload params) -> response
|
|
117
|
+
def self.archive: (String permalink) -> response
|
|
118
|
+
def self.render: (String permalink, ?payload template_model) -> response
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Streams < Resource
|
|
122
|
+
def list: () -> response
|
|
123
|
+
def create: (payload params) -> response
|
|
124
|
+
def get: (String permalink) -> response
|
|
125
|
+
def update: (String permalink, payload params) -> response
|
|
126
|
+
def archive: (String permalink) -> response
|
|
127
|
+
|
|
128
|
+
def self.list: () -> response
|
|
129
|
+
def self.create: (payload params) -> response
|
|
130
|
+
def self.get: (String permalink) -> response
|
|
131
|
+
def self.update: (String permalink, payload params) -> response
|
|
132
|
+
def self.archive: (String permalink) -> response
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class Stats < Resource
|
|
136
|
+
def get: (?from: String?, ?to: String?) -> response
|
|
137
|
+
def deliveries: () -> response
|
|
138
|
+
|
|
139
|
+
def self.get: (?from: String?, ?to: String?) -> response
|
|
140
|
+
def self.deliveries: () -> response
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class Bounces < Resource
|
|
144
|
+
def list: (**untyped filters) -> response
|
|
145
|
+
def get: (Integer | String id) -> response
|
|
146
|
+
|
|
147
|
+
def self.list: (**untyped filters) -> response
|
|
148
|
+
def self.get: (Integer | String id) -> response
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class Dmarc < Resource
|
|
152
|
+
def summary: (?domain: String?, ?from: String?, ?to: String?) -> response
|
|
153
|
+
def reports: (?domain: String?, ?from: String?, ?to: String?, ?page: Integer?, ?per_page: Integer?) -> response
|
|
154
|
+
def report: (Integer | String id) -> response
|
|
155
|
+
|
|
156
|
+
def self.summary: (?domain: String?, ?from: String?, ?to: String?) -> response
|
|
157
|
+
def self.reports: (?domain: String?, ?from: String?, ?to: String?, ?page: Integer?, ?per_page: Integer?) -> response
|
|
158
|
+
def self.report: (Integer | String id) -> response
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class Mailer
|
|
162
|
+
IGNORED_HEADERS: Array[String]
|
|
163
|
+
|
|
164
|
+
attr_accessor settings: Hash[Symbol, untyped]
|
|
165
|
+
|
|
166
|
+
def initialize: (?Hash[Symbol, untyped]? settings) -> void
|
|
167
|
+
def deliver!: (untyped mail) -> response
|
|
168
|
+
def build_params: (untyped mail) -> payload
|
|
169
|
+
end
|
|
170
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: camelmailer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Camelmailer contributors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby SDK for the Camelmailer transactional email API — send messages,
|
|
14
|
+
manage templates, streams and stats, and plug into Rails via the :camelmailer ActionMailer
|
|
15
|
+
delivery method.
|
|
16
|
+
email:
|
|
17
|
+
- hello@camelmailer.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- CONTRIBUTING.md
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.md
|
|
26
|
+
- lib/camelmailer.rb
|
|
27
|
+
- lib/camelmailer/bounces.rb
|
|
28
|
+
- lib/camelmailer/client.rb
|
|
29
|
+
- lib/camelmailer/dmarc.rb
|
|
30
|
+
- lib/camelmailer/emails.rb
|
|
31
|
+
- lib/camelmailer/errors.rb
|
|
32
|
+
- lib/camelmailer/mailer.rb
|
|
33
|
+
- lib/camelmailer/railtie.rb
|
|
34
|
+
- lib/camelmailer/resource.rb
|
|
35
|
+
- lib/camelmailer/stats.rb
|
|
36
|
+
- lib/camelmailer/streams.rb
|
|
37
|
+
- lib/camelmailer/templates.rb
|
|
38
|
+
- lib/camelmailer/version.rb
|
|
39
|
+
- sig/camelmailer.rbs
|
|
40
|
+
homepage: https://camelmailer.com
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
homepage_uri: https://camelmailer.com
|
|
45
|
+
source_code_uri: https://github.com/camelmailer/camelmailer-ruby
|
|
46
|
+
changelog_uri: https://github.com/camelmailer/camelmailer-ruby/blob/main/CHANGELOG.md
|
|
47
|
+
documentation_uri: https://camelmailer.com/docs
|
|
48
|
+
bug_tracker_uri: https://github.com/camelmailer/camelmailer-ruby/issues
|
|
49
|
+
rubygems_mfa_required: 'true'
|
|
50
|
+
post_install_message:
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '3.1'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.5.22
|
|
66
|
+
signing_key:
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: The Ruby and Rails SDK for Camelmailer
|
|
69
|
+
test_files: []
|