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 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
@@ -0,0 +1,4 @@
1
+ /spec/reports/
2
+ /.bundle/
3
+ /Gemfile.lock
4
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem "minitest", "~> 5.25"
9
+ gem "mail", ">= 2.8"
10
+ gem "base64", ">= 0.2"
11
+ end
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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groundmail
4
+ VERSION = "0.8.3"
5
+ 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: []