groundmail 0.8.3
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/.gitignore +4 -0
- data/Gemfile +11 -0
- data/README.md +33 -0
- data/groundmail.gemspec +35 -0
- data/lib/groundmail/client.rb +21 -0
- data/lib/groundmail/delivery_method.rb +46 -0
- data/lib/groundmail/error.rb +38 -0
- data/lib/groundmail/http.rb +51 -0
- data/lib/groundmail/message_mapper.rb +147 -0
- data/lib/groundmail/railtie.rb +14 -0
- data/lib/groundmail/resources.rb +201 -0
- data/lib/groundmail/version.rb +5 -0
- data/lib/groundmail.rb +28 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c04be29498d0ca9868ae52b53bcc0528b588f8a2d98f4f18cf20b0f39c6a25e2
|
|
4
|
+
data.tar.gz: 5586355da81959fc4f7d9979ca51edf10cb20feea7db7809d5f612740b4138d9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a3e6a375f80c44fd28e9a2f0236c60155b31a955772db2205276ceb3512594e97e5eb7b1171c03c9c50b3dcb88b1612e9a76d19a78cd91fa1d7e64290a5547e1
|
|
7
|
+
data.tar.gz: 736c04b506e1597cef883890bcdfbde1c5e9022aeb1c46f3abb5787018d18b04c49f67f72d4c37e1d0f8f849c96458c35c6811de41e88f5e53e95c5e6264ef1f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Groundmail Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby client for the [Groundmail](https://groundmail.dev) transactional email API.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gem install groundmail
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "groundmail"
|
|
11
|
+
|
|
12
|
+
tx = Groundmail::Client.new("gm_your_key_here")
|
|
13
|
+
|
|
14
|
+
result = tx.emails.send(
|
|
15
|
+
from: "you@yourdomain.com",
|
|
16
|
+
to: "user@example.com",
|
|
17
|
+
subject: "Hello",
|
|
18
|
+
html: "<p>It works.</p>"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
puts result["id"], result["status"]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Rails ActionMailer
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
config.action_mailer.delivery_method = :groundmail
|
|
28
|
+
config.action_mailer.groundmail_settings = {
|
|
29
|
+
api_key: ENV.fetch("GROUNDMAIL_API_KEY")
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Docs: https://docs.groundmail.dev/sdks/ruby
|
data/groundmail.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/groundmail/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "groundmail"
|
|
7
|
+
spec.version = Groundmail::VERSION
|
|
8
|
+
spec.authors = ["Groundmail"]
|
|
9
|
+
spec.email = ["support@groundmail.dev"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Groundmail transactional email API client for Ruby"
|
|
12
|
+
spec.description = "Official Ruby client for the Groundmail REST API."
|
|
13
|
+
spec.homepage = "https://groundmail.dev"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata = {
|
|
18
|
+
"homepage_uri" => spec.homepage,
|
|
19
|
+
"source_code_uri" => "https://github.com/Groundmail/groundmail-sdks/tree/main/ruby",
|
|
20
|
+
"documentation_uri" => "https://docs.groundmail.dev/sdks/ruby",
|
|
21
|
+
"rubygems_mfa_required" => "true"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.start_with?("spec/") }
|
|
26
|
+
rescue StandardError
|
|
27
|
+
Dir.glob("{lib}/**/*", base: __dir__) + ["README.md", "groundmail.gemspec"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
spec.add_dependency "base64", ">= 0.2"
|
|
33
|
+
spec.add_development_dependency "mail", ">= 2.8"
|
|
34
|
+
spec.add_development_dependency "minitest", "~> 5.25"
|
|
35
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Groundmail
|
|
4
|
+
class Client
|
|
5
|
+
attr_reader :emails, :domains, :webhooks, :templates, :suppressions
|
|
6
|
+
|
|
7
|
+
def initialize(api_key, base_url: HttpClient::DEFAULT_API_BASE_URL, transport: nil)
|
|
8
|
+
http = HttpClient.new(api_key, base_url: base_url, transport: transport)
|
|
9
|
+
@emails = EmailsResource.new(http)
|
|
10
|
+
@domains = DomainsResource.new(http)
|
|
11
|
+
@webhooks = WebhooksResource.new(http)
|
|
12
|
+
@templates = TemplatesResource.new(http)
|
|
13
|
+
@suppressions = SuppressionsResource.new(http)
|
|
14
|
+
@http = http
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def request(method, path, body: nil, headers: {})
|
|
18
|
+
@http.request(method, path, body: body, headers: headers)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mail"
|
|
4
|
+
require_relative "message_mapper"
|
|
5
|
+
|
|
6
|
+
module Groundmail
|
|
7
|
+
# ActionMailer delivery method: config.action_mailer.delivery_method = :groundmail
|
|
8
|
+
class DeliveryMethod
|
|
9
|
+
def initialize(settings = {})
|
|
10
|
+
@settings = settings || {}
|
|
11
|
+
@client = @settings[:client] || build_client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def deliver!(mail)
|
|
15
|
+
params = MessageMapper.to_send_params(mail)
|
|
16
|
+
result = @client.emails.send(**params)
|
|
17
|
+
message_id = result.is_a?(Hash) ? (result["id"] || result[:id]) : nil
|
|
18
|
+
mail.message_id = message_id if message_id && mail.respond_to?(:message_id=)
|
|
19
|
+
result
|
|
20
|
+
rescue Groundmail::Error => e
|
|
21
|
+
# Re-raise as Groundmail::Error (not a bare String → RuntimeError) so
|
|
22
|
+
# ActionMailer / rescue_from can read status, details, and error_code.
|
|
23
|
+
raise Groundmail::Error.new(
|
|
24
|
+
"Groundmail API error: #{e.message}",
|
|
25
|
+
status: e.status,
|
|
26
|
+
details: e.details,
|
|
27
|
+
error_code: e.error_code,
|
|
28
|
+
upgrade_url: e.upgrade_url
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_client
|
|
35
|
+
api_key = @settings[:api_key] || @settings["api_key"] || ENV["GROUNDMAIL_API_KEY"]
|
|
36
|
+
raise ArgumentError, "GROUNDMAIL_API_KEY is not configured." if api_key.nil? || api_key.to_s.empty?
|
|
37
|
+
|
|
38
|
+
base_url = @settings[:base_url] || @settings["base_url"] || ENV["GROUNDMAIL_BASE_URL"]
|
|
39
|
+
if base_url && !base_url.to_s.empty?
|
|
40
|
+
Client.new(api_key, base_url: base_url)
|
|
41
|
+
else
|
|
42
|
+
Client.new(api_key)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Groundmail
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
attr_reader :status, :details, :error_code, :upgrade_url
|
|
10
|
+
|
|
11
|
+
def initialize(message, status:, details: nil, error_code: nil, upgrade_url: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@status = status
|
|
14
|
+
@details = details
|
|
15
|
+
@error_code = error_code
|
|
16
|
+
@upgrade_url = upgrade_url
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_response(status, body)
|
|
20
|
+
err = body.is_a?(Hash) ? body["error"] : nil
|
|
21
|
+
|
|
22
|
+
case err
|
|
23
|
+
when String
|
|
24
|
+
new(err, status: status)
|
|
25
|
+
when Hash
|
|
26
|
+
new(
|
|
27
|
+
err["message"] || "Request failed with status #{status}",
|
|
28
|
+
status: status,
|
|
29
|
+
details: err["details"],
|
|
30
|
+
error_code: err["code"]&.to_s,
|
|
31
|
+
upgrade_url: err["upgrade_url"]&.to_s
|
|
32
|
+
)
|
|
33
|
+
else
|
|
34
|
+
new("Request failed with status #{status}", status: status)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Groundmail
|
|
8
|
+
class HttpClient
|
|
9
|
+
DEFAULT_API_BASE_URL = "https://api.groundmail.dev"
|
|
10
|
+
|
|
11
|
+
def initialize(api_key, base_url: DEFAULT_API_BASE_URL, transport: nil)
|
|
12
|
+
raise ArgumentError, "Groundmail API key must start with gm_" unless api_key.start_with?("gm_")
|
|
13
|
+
|
|
14
|
+
@api_key = api_key
|
|
15
|
+
@base_url = base_url.chomp("/")
|
|
16
|
+
@transport = transport
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def request(method, path, body: nil, headers: {})
|
|
20
|
+
request_headers = {
|
|
21
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
22
|
+
"Content-Type" => "application/json"
|
|
23
|
+
}.merge(headers)
|
|
24
|
+
|
|
25
|
+
return @transport.call(method, path, body, request_headers) if @transport
|
|
26
|
+
|
|
27
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
28
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
29
|
+
http.use_ssl = uri.scheme == "https"
|
|
30
|
+
|
|
31
|
+
request_class = Net::HTTP.const_get(method.capitalize)
|
|
32
|
+
request = request_class.new(uri.request_uri)
|
|
33
|
+
request_headers.each { |key, value| request[key] = value }
|
|
34
|
+
request.body = JSON.generate(body) if body
|
|
35
|
+
|
|
36
|
+
response = http.request(request)
|
|
37
|
+
parsed = response.body.to_s.empty? ? {} : JSON.parse(response.body)
|
|
38
|
+
|
|
39
|
+
raise Error.from_response(response.code.to_i, parsed) if response.code.to_i >= 400
|
|
40
|
+
|
|
41
|
+
parsed
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def query(params)
|
|
45
|
+
filtered = params.compact
|
|
46
|
+
return "" if filtered.empty?
|
|
47
|
+
|
|
48
|
+
"?#{URI.encode_www_form(filtered)}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Groundmail
|
|
6
|
+
# Maps a Mail::Message into emails.send keyword args.
|
|
7
|
+
module MessageMapper
|
|
8
|
+
HEADER_IDEMPOTENCY = "X-Groundmail-Idempotency-Key"
|
|
9
|
+
HEADER_SCHEDULED_AT = "X-Groundmail-Scheduled-At"
|
|
10
|
+
HEADER_TAG = "X-Groundmail-Tag"
|
|
11
|
+
|
|
12
|
+
RESERVED_HEADERS = %w[
|
|
13
|
+
from to cc bcc reply-to subject content-type mime-version date message-id
|
|
14
|
+
x-groundmail-idempotency-key x-groundmail-scheduled-at x-groundmail-tag
|
|
15
|
+
idempotency-key
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def to_send_params(mail)
|
|
21
|
+
from = format_addresses(mail[:from])
|
|
22
|
+
raise ArgumentError, "Email is missing a From address." if from.empty?
|
|
23
|
+
|
|
24
|
+
to = format_addresses(mail[:to])
|
|
25
|
+
raise ArgumentError, "Email is missing a To recipient." if to.empty?
|
|
26
|
+
|
|
27
|
+
html = html_body(mail)
|
|
28
|
+
text = text_body(mail)
|
|
29
|
+
raise ArgumentError, "Email must include html or text content." if html.nil? && text.nil?
|
|
30
|
+
|
|
31
|
+
params = {
|
|
32
|
+
from: from.length == 1 ? from[0] : from,
|
|
33
|
+
to: to.length == 1 ? to[0] : to
|
|
34
|
+
}
|
|
35
|
+
params[:subject] = mail.subject if mail.subject && !mail.subject.empty?
|
|
36
|
+
params[:html] = html if html
|
|
37
|
+
params[:text] = text if text
|
|
38
|
+
|
|
39
|
+
reply_to = format_addresses(mail[:reply_to])
|
|
40
|
+
params[:reply_to] = reply_to.length == 1 ? reply_to[0] : reply_to unless reply_to.empty?
|
|
41
|
+
|
|
42
|
+
cc = format_addresses(mail[:cc])
|
|
43
|
+
params[:cc] = cc unless cc.empty?
|
|
44
|
+
bcc = format_addresses(mail[:bcc])
|
|
45
|
+
params[:bcc] = bcc unless bcc.empty?
|
|
46
|
+
|
|
47
|
+
attachments = map_attachments(mail)
|
|
48
|
+
params[:attachments] = attachments unless attachments.empty?
|
|
49
|
+
|
|
50
|
+
tags = extract_tags(mail)
|
|
51
|
+
params[:tags] = tags unless tags.empty?
|
|
52
|
+
|
|
53
|
+
headers = extract_forward_headers(mail)
|
|
54
|
+
params[:headers] = headers unless headers.empty?
|
|
55
|
+
|
|
56
|
+
idem = header_value(mail, HEADER_IDEMPOTENCY) || header_value(mail, "Idempotency-Key")
|
|
57
|
+
params[:idempotency_key] = idem if idem
|
|
58
|
+
|
|
59
|
+
scheduled = header_value(mail, HEADER_SCHEDULED_AT)
|
|
60
|
+
params[:scheduled_at] = scheduled if scheduled
|
|
61
|
+
|
|
62
|
+
params
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def format_addresses(field)
|
|
66
|
+
return [] if field.nil?
|
|
67
|
+
|
|
68
|
+
Array(field.addrs).filter_map do |addr|
|
|
69
|
+
email = addr.address.to_s.strip
|
|
70
|
+
next if email.empty?
|
|
71
|
+
|
|
72
|
+
name = addr.display_name.to_s.strip
|
|
73
|
+
name.empty? ? email : "#{name} <#{email}>"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def html_body(mail)
|
|
78
|
+
if mail.html_part
|
|
79
|
+
mail.html_part.decoded
|
|
80
|
+
elsif mail.mime_type == "text/html"
|
|
81
|
+
mail.decoded
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def text_body(mail)
|
|
86
|
+
if mail.text_part
|
|
87
|
+
mail.text_part.decoded
|
|
88
|
+
elsif mail.mime_type == "text/plain" || (!mail.multipart? && mail.mime_type.nil?)
|
|
89
|
+
body = mail.body.decoded
|
|
90
|
+
body && !body.empty? ? body : nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def map_attachments(mail)
|
|
95
|
+
mail.attachments.map do |attachment|
|
|
96
|
+
{
|
|
97
|
+
"filename" => attachment.filename.to_s.empty? ? "attachment" : attachment.filename,
|
|
98
|
+
"content_type" => attachment.mime_type || "application/octet-stream",
|
|
99
|
+
"content" => Base64.strict_encode64(attachment.body.decoded)
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extract_tags(mail)
|
|
105
|
+
values = mail.header[HEADER_TAG]
|
|
106
|
+
return [] unless values
|
|
107
|
+
|
|
108
|
+
list = values.respond_to?(:map) ? values.map(&:to_s) : [values.to_s]
|
|
109
|
+
list.filter_map do |raw|
|
|
110
|
+
trimmed = raw.strip
|
|
111
|
+
next if trimmed.empty?
|
|
112
|
+
|
|
113
|
+
if trimmed.include?("=")
|
|
114
|
+
name, value = trimmed.split("=", 2)
|
|
115
|
+
next if name.strip.empty? || value.strip.empty?
|
|
116
|
+
|
|
117
|
+
{ "name" => name.strip, "value" => value.strip }
|
|
118
|
+
else
|
|
119
|
+
{ "name" => "tag", "value" => trimmed }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def extract_forward_headers(mail)
|
|
125
|
+
out = {}
|
|
126
|
+
mail.header.fields.each do |field|
|
|
127
|
+
name = field.name.to_s
|
|
128
|
+
lower = name.downcase
|
|
129
|
+
next if RESERVED_HEADERS.include?(lower)
|
|
130
|
+
next if lower.start_with?("content-")
|
|
131
|
+
|
|
132
|
+
value = field.value.to_s.strip
|
|
133
|
+
out[name] = value unless value.empty?
|
|
134
|
+
end
|
|
135
|
+
out
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def header_value(mail, name)
|
|
139
|
+
field = mail.header[name]
|
|
140
|
+
return nil unless field
|
|
141
|
+
|
|
142
|
+
value = field.respond_to?(:last) ? field.last.to_s : field.to_s
|
|
143
|
+
value = value.strip
|
|
144
|
+
value.empty? ? nil : value
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
require_relative "delivery_method"
|
|
5
|
+
|
|
6
|
+
module Groundmail
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
initializer "groundmail.add_delivery_method" do
|
|
9
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
10
|
+
ActionMailer::Base.add_delivery_method :groundmail, Groundmail::DeliveryMethod
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Groundmail
|
|
4
|
+
class EmailsResource
|
|
5
|
+
def initialize(http)
|
|
6
|
+
@http = http
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def list(limit: nil, cursor: nil)
|
|
10
|
+
@http.request("GET", "/emails#{@http.query({ "limit" => limit, "cursor" => cursor })}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(email_id)
|
|
14
|
+
@http.request("GET", "/emails/#{URI.encode_www_form_component(email_id)}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send(from: nil, to: nil, **params)
|
|
18
|
+
params["from"] = from if from
|
|
19
|
+
params["to"] = to if to
|
|
20
|
+
body = serialize_send_params(params)
|
|
21
|
+
headers = {}
|
|
22
|
+
idempotency_key =
|
|
23
|
+
params["idempotency_key"] || params[:idempotency_key] ||
|
|
24
|
+
params["idempotencyKey"] || params[:idempotencyKey]
|
|
25
|
+
headers["Idempotency-Key"] = idempotency_key.to_s if idempotency_key
|
|
26
|
+
|
|
27
|
+
@http.request("POST", "/emails", body: body, headers: headers)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def batch(emails)
|
|
31
|
+
serialized = emails.map { |email| serialize_send_params(email) }
|
|
32
|
+
@http.request("POST", "/emails/batch", body: { "emails" => serialized })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cancel(email_id)
|
|
36
|
+
@http.request("PATCH", "/emails/#{URI.encode_www_form_component(email_id)}", body: { "cancel" => true })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def resend(email_id)
|
|
40
|
+
@http.request("POST", "/emails/#{URI.encode_www_form_component(email_id)}/resend")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_webhook(**params)
|
|
44
|
+
@http.request("POST", "/emails/test", body: params)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def insights(window: "30d")
|
|
48
|
+
@http.request("GET", "/deliverability#{@http.query({ "window" => window })}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def serialize_send_params(params)
|
|
54
|
+
body = {
|
|
55
|
+
"from" => params["from"] || params[:from],
|
|
56
|
+
"to" => params["to"] || params[:to]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
%w[subject html text reply_to replyTo cc bcc tags headers tag template attachments].each do |key|
|
|
60
|
+
value = params[key] || params[key.to_sym]
|
|
61
|
+
next if value.nil?
|
|
62
|
+
|
|
63
|
+
mapped = key == "replyTo" ? "reply_to" : key
|
|
64
|
+
body[mapped] = value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
body["html"] = params["htmlBody"] || params[:htmlBody] if params["htmlBody"] || params[:htmlBody]
|
|
68
|
+
body["text"] = params["textBody"] || params[:textBody] if params["textBody"] || params[:textBody]
|
|
69
|
+
body["scheduled_at"] = params["scheduled_at"] || params[:scheduled_at] if params["scheduled_at"] || params[:scheduled_at]
|
|
70
|
+
body["scheduled_at"] ||= params["scheduledAt"] || params[:scheduledAt]
|
|
71
|
+
body["unsubscribe"] = params["unsubscribe"] || params[:unsubscribe] if params.key?("unsubscribe") || params.key?(:unsubscribe)
|
|
72
|
+
|
|
73
|
+
body
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class DomainsResource
|
|
78
|
+
def initialize(http)
|
|
79
|
+
@http = http
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def list(limit: nil, cursor: nil, inbound_enabled: nil)
|
|
83
|
+
@http.request(
|
|
84
|
+
"GET",
|
|
85
|
+
"/domains#{@http.query({ "limit" => limit, "cursor" => cursor, "inbound_enabled" => inbound_enabled })}"
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get(id_or_name)
|
|
90
|
+
@http.request("GET", "/domains/#{URI.encode_www_form_component(id_or_name)}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def create(name, inbound_enabled: nil)
|
|
94
|
+
body = { "name" => name }
|
|
95
|
+
body["inbound_enabled"] = inbound_enabled unless inbound_enabled.nil?
|
|
96
|
+
@http.request("POST", "/domains", body: body)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def verify(id_or_name)
|
|
100
|
+
@http.request("POST", "/domains/#{URI.encode_www_form_component(id_or_name)}", body: { "action" => "verify" })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def apply(id_or_name, provider: "cloudflare", credentials: nil)
|
|
104
|
+
body = { "action" => "apply", "provider" => provider }
|
|
105
|
+
body["credentials"] = credentials if credentials
|
|
106
|
+
@http.request("POST", "/domains/#{URI.encode_www_form_component(id_or_name)}", body: body)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def update(id_or_name, **params)
|
|
110
|
+
@http.request("PATCH", "/domains/#{URI.encode_www_form_component(id_or_name)}", body: params)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def delete(id_or_name)
|
|
114
|
+
@http.request("DELETE", "/domains/#{URI.encode_www_form_component(id_or_name)}")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class WebhooksResource
|
|
119
|
+
def initialize(http)
|
|
120
|
+
@http = http
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def list(limit: nil, cursor: nil)
|
|
124
|
+
@http.request("GET", "/webhooks#{@http.query({ "limit" => limit, "cursor" => cursor })}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def get(webhook_id)
|
|
128
|
+
@http.request("GET", "/webhooks/#{URI.encode_www_form_component(webhook_id)}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def create(**params)
|
|
132
|
+
@http.request("POST", "/webhooks", body: params)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def update(webhook_id, **params)
|
|
136
|
+
@http.request("PATCH", "/webhooks/#{URI.encode_www_form_component(webhook_id)}", body: params)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def delete(webhook_id)
|
|
140
|
+
@http.request("DELETE", "/webhooks/#{URI.encode_www_form_component(webhook_id)}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class TemplatesResource
|
|
145
|
+
def initialize(http)
|
|
146
|
+
@http = http
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def list(limit: nil, cursor: nil, status: nil)
|
|
150
|
+
@http.request(
|
|
151
|
+
"GET",
|
|
152
|
+
"/templates#{@http.query({ "limit" => limit, "cursor" => cursor, "status" => status })}"
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get(id_or_alias)
|
|
157
|
+
@http.request("GET", "/templates/#{URI.encode_www_form_component(id_or_alias)}")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def create(**params)
|
|
161
|
+
@http.request("POST", "/templates", body: params)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def update(id_or_alias, **params)
|
|
165
|
+
@http.request("PATCH", "/templates/#{URI.encode_www_form_component(id_or_alias)}", body: params)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def delete(id_or_alias)
|
|
169
|
+
@http.request("DELETE", "/templates/#{URI.encode_www_form_component(id_or_alias)}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def publish(id_or_alias)
|
|
173
|
+
@http.request("POST", "/templates/#{URI.encode_www_form_component(id_or_alias)}", body: { "action" => "publish" })
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class SuppressionsResource
|
|
178
|
+
def initialize(http)
|
|
179
|
+
@http = http
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def list(limit: nil, cursor: nil, email: nil)
|
|
183
|
+
@http.request(
|
|
184
|
+
"GET",
|
|
185
|
+
"/suppressions#{@http.query({ "limit" => limit, "cursor" => cursor, "email" => email })}"
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def create(**params)
|
|
190
|
+
@http.request("POST", "/suppressions", body: params)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def remove(id_or_email)
|
|
194
|
+
if id_or_email.include?("@")
|
|
195
|
+
return @http.request("DELETE", "/suppressions#{@http.query({ "email" => id_or_email })}")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
@http.request("DELETE", "/suppressions/#{URI.encode_www_form_component(id_or_email)}")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
data/lib/groundmail.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "groundmail/version"
|
|
4
|
+
require_relative "groundmail/error"
|
|
5
|
+
require_relative "groundmail/http"
|
|
6
|
+
require_relative "groundmail/resources"
|
|
7
|
+
require_relative "groundmail/client"
|
|
8
|
+
|
|
9
|
+
module Groundmail
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
require "base64"
|
|
14
|
+
require "mail"
|
|
15
|
+
rescue LoadError
|
|
16
|
+
# base64/mail are optional; required for ActionMailer delivery method.
|
|
17
|
+
else
|
|
18
|
+
require_relative "groundmail/message_mapper"
|
|
19
|
+
require_relative "groundmail/delivery_method"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
require "rails"
|
|
24
|
+
rescue LoadError
|
|
25
|
+
# Rails is optional.
|
|
26
|
+
else
|
|
27
|
+
require_relative "groundmail/railtie"
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: groundmail
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Groundmail
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: mail
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.8'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.8'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.25'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.25'
|
|
55
|
+
description: Official Ruby client for the Groundmail REST API.
|
|
56
|
+
email:
|
|
57
|
+
- support@groundmail.dev
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- ".gitignore"
|
|
63
|
+
- Gemfile
|
|
64
|
+
- README.md
|
|
65
|
+
- groundmail.gemspec
|
|
66
|
+
- lib/groundmail.rb
|
|
67
|
+
- lib/groundmail/client.rb
|
|
68
|
+
- lib/groundmail/delivery_method.rb
|
|
69
|
+
- lib/groundmail/error.rb
|
|
70
|
+
- lib/groundmail/http.rb
|
|
71
|
+
- lib/groundmail/message_mapper.rb
|
|
72
|
+
- lib/groundmail/railtie.rb
|
|
73
|
+
- lib/groundmail/resources.rb
|
|
74
|
+
- lib/groundmail/version.rb
|
|
75
|
+
homepage: https://groundmail.dev
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://groundmail.dev
|
|
80
|
+
source_code_uri: https://github.com/Groundmail/groundmail-sdks/tree/main/ruby
|
|
81
|
+
documentation_uri: https://docs.groundmail.dev/sdks/ruby
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
83
|
+
post_install_message:
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 3.1.0
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.5.22
|
|
99
|
+
signing_key:
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Groundmail transactional email API client for Ruby
|
|
102
|
+
test_files: []
|