mailkube-rails 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f536b33be8802fb5082f7ff0728c24cdd6e0b236a8b1f1f9be280458a33ea7c9
4
+ data.tar.gz: a644b374eae8bccaa8efd2ab896ee3adec93e57a46857533ae66c7f20e32dcd5
5
+ SHA512:
6
+ metadata.gz: dcf1c48032b57f0646d8ffb76cf3d4442176af146a8a2bcb54012dec515ccd36adc1365ead40bd48eceddab10816a14180c1274e9d75ec04feb791ad6502f1a6
7
+ data.tar.gz: a100a9dc37811df1eb70c9f42a5e15c70437a1a79f9f235f427e8e0a03b75adc46274b85f0aee49bdecf6d84c04c27f0ce0ff8104aef69705479e8af0be2a231
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or Derivative
95
+ Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Mailtactic, Corp.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/NOTICE ADDED
@@ -0,0 +1,11 @@
1
+ mailkube-rails
2
+ Copyright 2026 Mailtactic, Corp.
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this product except in compliance with the License. You may
6
+ obtain a copy of the License at:
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ "mailkube" is a trademark of Mailtactic, Corp. Use of the name is governed by
11
+ Section 6 (Trademarks) of the License.
data/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # mailkube-rails
2
+
3
+ [![CI](https://github.com/mailkube/mailkube-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/mailkube/mailkube-rails/actions/workflows/ci.yml)
4
+ [![Gem](https://img.shields.io/gem/v/mailkube-rails)](https://rubygems.org/gems/mailkube-rails)
5
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
6
+ [![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-2.1-purple.svg)](CODE_OF_CONDUCT.md)
7
+
8
+ ActionMailer delivery method for mailkube.
9
+
10
+ Send mail through mailkube using ActionMailer exactly as you already do, and receive webhooks as
11
+ `ActiveSupport::Notifications`. This gem is a thin adapter over the
12
+ [`mailkube`](https://rubygems.org/gems/mailkube) gem: the API, retries,
13
+ errors and signature verification all live there.
14
+
15
+ Requires Ruby 3.4+ and Rails 7.2.3+.
16
+
17
+ ## Install
18
+
19
+ ```ruby
20
+ # Gemfile
21
+ gem "mailkube-rails"
22
+ ```
23
+
24
+ Then point ActionMailer at it:
25
+
26
+ ```ruby
27
+ # config/environments/production.rb
28
+ config.action_mailer.delivery_method = :mailkube
29
+ config.action_mailer.mailkube_settings = {
30
+ api_key: ENV.fetch("MAILKUBE_API_KEY")
31
+ }
32
+ ```
33
+
34
+ That is the whole setup. Everything else on this page is optional.
35
+
36
+ ## Sending
37
+
38
+ Nothing about your application changes. Mailers, `deliver_now`, `deliver_later`, attachments,
39
+ `ActionMailer::Base.deliveries` in tests: all of it works as it already did.
40
+
41
+ ```ruby
42
+ class OrderMailer < ApplicationMailer
43
+ def shipped(order)
44
+ mail(to: order.customer_email, subject: "Your order shipped")
45
+ end
46
+ end
47
+
48
+ OrderMailer.shipped(order).deliver_later
49
+ ```
50
+
51
+ ### What is mapped
52
+
53
+ The sender, recipients (including blind copies), subject, both bodies, attachments and your custom
54
+ headers. Anything ActionMailer's message can express, this delivery method passes through.
55
+
56
+ Send-time features the SDK offers but `Mail::Message` has no slot for — tags, topics, templates,
57
+ scheduling, idempotency keys — are reached by using the SDK directly:
58
+
59
+ ```ruby
60
+ Mailkube.new.emails.send(
61
+ from: "Acme <hello@yourdomain.com>",
62
+ to: "customer@example.com",
63
+ subject: "Hello world",
64
+ html: "<p>It works!</p>",
65
+ tags: [Mailkube::Tag.new(name: "campaign", value: "spring")]
66
+ )
67
+ ```
68
+
69
+ > **Inline images degrade to ordinary attachments.** The API's attachment model carries a filename,
70
+ > content and content type, with no content id, so a `cid:` reference has nothing to resolve
71
+ > against. This is a capability of the platform rather than of this gem.
72
+
73
+ ## Configuration
74
+
75
+ Two homes, and they do not overlap, so there is nothing to resolve between them.
76
+
77
+ | Setting | Where | Environment fallback | Default |
78
+ |---|---|---|---|
79
+ | API key | `config.action_mailer.mailkube_settings[:api_key]` | `MAILKUBE_API_KEY` | required |
80
+ | Base URL | `config.action_mailer.mailkube_settings[:base_url]` | `MAILKUBE_BASE_URL` | the SDK's |
81
+ | Timeout | `config.action_mailer.mailkube_settings[:timeout]` | | the SDK's |
82
+ | Webhook secret | `config.mailkube.webhook_secret` | | required for webhooks |
83
+ | Webhook tolerance | `config.mailkube.webhook_tolerance` | | the SDK's |
84
+
85
+ An unset setting is **omitted** rather than passed along as nil, so the SDK's own environment
86
+ resolution still applies. That is why the API key can come from `MAILKUBE_API_KEY`
87
+ alone with no Rails configuration at all.
88
+
89
+ ### Two accounts, two mailers
90
+
91
+ ActionMailer resolves the delivery method per message, so overriding the settings on one mailer
92
+ gives it its own client:
93
+
94
+ ```ruby
95
+ class MarketingMailer < ApplicationMailer
96
+ self.mailkube_settings = { api_key: ENV.fetch("MARKETING_API_KEY") }
97
+ end
98
+ ```
99
+
100
+ ## Errors
101
+
102
+ A failed delivery raises `Mailkube::Rails::DeliveryError`, with the SDK's own exception preserved as
103
+ `cause`.
104
+
105
+ ```ruby
106
+ class OrderMailer < ApplicationMailer
107
+ retry_on Mailkube::Rails::DeliveryError, wait: :polynomially_longer, attempts: 5
108
+ end
109
+ ```
110
+
111
+ The class this gem owns is what you name, deliberately: naming an SDK class would tie your retry
112
+ policy to the SDK's exception hierarchy, and it would break the day that hierarchy was reorganized.
113
+ For the detail — which error, which status, whether it is safe to retry — read `cause`, where the
114
+ SDK defines the categories once:
115
+
116
+ ```ruby
117
+ rescue Mailkube::Rails::DeliveryError => e
118
+ Rails.logger.warn(e.cause.error_name) # e.g. "quota_exceeded", with status_code and request_id
119
+ end
120
+ ```
121
+
122
+ `config.action_mailer.raise_delivery_errors = false` still swallows failures exactly as it does for
123
+ any other delivery method.
124
+
125
+ ## Webhooks
126
+
127
+ You write the route. A mountable engine would force a path on your application and drag generator
128
+ machinery along for one endpoint, so this gem ships the controller and lets you mount it where you
129
+ want:
130
+
131
+ ```ruby
132
+ # config/initializers/mailkube.rb
133
+ Rails.application.config.mailkube.webhook_secret = ENV.fetch("MAILKUBE_WEBHOOK_SECRET")
134
+ ```
135
+
136
+ ```ruby
137
+ # config/routes.rb
138
+ require "mailkube/rails/webhooks_controller"
139
+
140
+ Rails.application.routes.draw do
141
+ post "/webhooks/mailkube",
142
+ to: Mailkube::Rails::WebhooksController.action(:create)
143
+ end
144
+ ```
145
+
146
+ The `require` is needed and the `.action(:create)` form is deliberate: a gem's `lib/` is on the load
147
+ path but is not an autoload path, so the `"controller#action"` string form would name a constant
148
+ Zeitwerk has never been told about.
149
+
150
+ Deliveries are verified against the **raw** request body and published as one notification:
151
+
152
+ ```ruby
153
+ # config/initializers/mailkube.rb
154
+ ActiveSupport::Notifications.subscribe("webhook.mailkube") do |*, payload|
155
+ event = payload[:event]
156
+
157
+ case event
158
+ when Mailkube::Events::EmailDeliveredEvent then Rails.logger.info("delivered #{event.data.email_id}")
159
+ when Mailkube::Events::EmailBouncedEvent then Suppression.add(event.data.email_id)
160
+ end
161
+ end
162
+ ```
163
+
164
+ One notification rather than one per webhook type, so an event this SDK release does not model still
165
+ reaches your subscriber with its payload intact instead of being dropped.
166
+
167
+ Subscribers run **inside the request**, and the endpoint answers as soon as they return. Enqueue
168
+ anything slow, or delivery latency becomes retries.
169
+
170
+ The endpoint answers `204` on success, `400` when verification fails, and `500` when no secret is
171
+ configured — the last so that the platform keeps retrying rather than discarding deliveries while
172
+ your configuration is broken. CSRF is skipped on this controller because a machine-to-machine POST
173
+ cannot present a token; the signature is what authenticates the request, and it is strictly
174
+ stronger. Any middleware that decodes and re-encodes the request body breaks verification: it no
175
+ longer covers the bytes that were signed.
176
+
177
+ ## Extending this gem
178
+
179
+ Before adding a setting, a mapped field, or another entry point, read
180
+ [`.rules/INTEGRATION_CONTRACT.md`](.rules/INTEGRATION_CONTRACT.md) (what every mailkube framework
181
+ integration does identically) and [`.rules/RAILS_INTEGRATION.md`](.rules/RAILS_INTEGRATION.md)
182
+ (how those rules land here). Both carry a checklist.
183
+
184
+ The short version: the capability has to exist in the SDK first, inputs are mapped in `payload.rb`
185
+ and never at a call site, and configuration goes through `config.rb`.
186
+
187
+ This gem ships no `examples/` directory, deliberately: every entry point it exposes needs a host
188
+ Rails application before it can run. This README is the wiring documentation, and the spec suite is
189
+ what keeps it honest.
190
+
191
+ ## Contributing
192
+
193
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup and the quality gates every change
194
+ must pass. Security issues: see [SECURITY.md](SECURITY.md).
195
+
196
+ ## License
197
+
198
+ [Apache-2.0](LICENSE) © 2026 Mailtactic, Corp.
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ module Rails
5
+ # The one place this gem turns Rails settings into SDK constructor arguments.
6
+ #
7
+ # Everything that reaches the SDK goes through {build_client}: the delivery method, the webhook
8
+ # endpoint, and anything added later. Two call sites building their own client is how one of
9
+ # them ends up on a different base URL, or without the User-Agent suffix, and the difference
10
+ # shows up as a support question rather than as a failing test.
11
+ #
12
+ # ## Two homes, and no precedence rules
13
+ #
14
+ # Settings live in the two places Rails already puts these kinds of settings, and they do not
15
+ # overlap, so there is nothing to resolve between them:
16
+ #
17
+ # - `config.action_mailer.mailkube_settings` — the delivery credentials. This hash is
18
+ # what `add_delivery_method` creates, and ActionMailer hands it to the delivery method for
19
+ # every message, so it is the idiomatic home and needs no invention.
20
+ # - `config.mailkube` — the webhook secret and freshness window, on the
21
+ # `ActiveSupport::OrderedOptions` the Railtie installs. These are not delivery settings and
22
+ # putting them under `action_mailer` would misfile them.
23
+ module Config
24
+ # SDK constructor keywords, mapped to the settings key that answers each.
25
+ #
26
+ # One list, so a setting cannot be readable in the delivery method and quietly ignored in
27
+ # a startup check. Adding an SDK keyword is a row here and nothing else.
28
+ CLIENT_KEYS = { api_key: :api_key, base_url: :base_url, timeout: :timeout }.freeze
29
+
30
+ # Build an SDK client from a delivery-method settings hash.
31
+ #
32
+ # @param settings [Hash{Symbol => Object}, nil] the `mailkube_settings` hash.
33
+ # @return [Mailkube::Client] the client, carrying this gem's User-Agent suffix.
34
+ # @raise [Mailkube::ConfigurationError] when no API key is available anywhere.
35
+ def self.build_client(settings)
36
+ # Splatted as keywords: a setting this gem did not resolve is simply not passed, which is
37
+ # what leaves the SDK's own default and its environment fallback in charge. Passing an
38
+ # explicit nil would suppress that fallback while looking like configuration.
39
+ Mailkube::Client.new(**client_kwargs(settings), user_agent_suffix: user_agent_suffix)
40
+ end
41
+
42
+ # Resolve the SDK keywords that are actually set, dropping the rest.
43
+ #
44
+ # @param settings [Hash{Symbol => Object}, nil] the `mailkube_settings` hash.
45
+ # @return [Hash{Symbol => Object}] the keywords to pass to the SDK.
46
+ def self.client_kwargs(settings)
47
+ given = settings || {}
48
+ kwargs = {} #: Hash[Symbol, untyped]
49
+ CLIENT_KEYS.each do |keyword, key|
50
+ value = given[key]
51
+ # An empty string is treated as unset. It is what an unset `ENV["..."]` interpolated
52
+ # into an initializer produces, and passing it through would defeat the SDK's fallback
53
+ # while looking like a configured value.
54
+ next if value.nil? || value == ""
55
+
56
+ kwargs[keyword] = value
57
+ end
58
+ kwargs
59
+ end
60
+
61
+ # This gem's own `name/version` token for the SDK's User-Agent.
62
+ #
63
+ # Read from {VERSION} rather than written here. A literal would be a second source of truth
64
+ # that the release process does not update, so it would go stale on the first release and
65
+ # stay wrong for every one after it. The SDK's own token stays leading, so the result is
66
+ # `mailkube/1.1.0 mailkube-rails/0.1.0`.
67
+ #
68
+ # @return [String] the suffix token.
69
+ def self.user_agent_suffix = "mailkube-rails/#{VERSION}"
70
+
71
+ # The webhook signing secret, or nil when none is configured.
72
+ #
73
+ # @param app_config [Object] an object responding to `mailkube`, normally `::Rails.application.config`.
74
+ # @return [String, nil] the secret, or nil.
75
+ def self.webhook_secret(app_config)
76
+ value = options(app_config).webhook_secret
77
+ value.is_a?(String) && !value.empty? ? value : nil
78
+ end
79
+
80
+ # The signature freshness tolerance, or nil to accept the SDK's documented default.
81
+ #
82
+ # @param app_config [Object] an object responding to `mailkube`.
83
+ # @return [Integer, nil] the window in seconds, or nil.
84
+ def self.webhook_tolerance(app_config)
85
+ value = options(app_config).webhook_tolerance
86
+ value.nil? ? nil : Integer(value)
87
+ end
88
+
89
+ # The gem's own options block, tolerating an application that never set one.
90
+ #
91
+ # `ActiveSupport::OrderedOptions` answers nil for any unset key, so this returns an empty one
92
+ # rather than nil and every reader above stays a single expression.
93
+ #
94
+ # @param app_config [Object] an object responding to `mailkube`.
95
+ # @return [ActiveSupport::OrderedOptions] the options block.
96
+ def self.options(app_config)
97
+ app_config.mailkube || ActiveSupport::OrderedOptions.new
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ module Rails
5
+ # The ActionMailer delivery method: the outbound entry point, and error translation.
6
+ #
7
+ # ## The protocol this implements
8
+ #
9
+ # `Mail` instantiates a delivery method **once per message**, with one positional settings
10
+ # hash (`mail/message.rb`: `lookup_delivery_method(method).new(settings)`), then calls
11
+ # `deliver!(mail)`. Two consequences shape this class:
12
+ #
13
+ # - `settings` must be readable back off the instance. `Mail::Message#deliver!` evaluates
14
+ # `delivery_method.settings[:return_response]`, so a delivery method whose `settings` is nil
15
+ # raises there rather than delivering. It is a real requirement, not a convention.
16
+ # - The instance is per-message and short-lived, so the SDK client is memoized on it and that
17
+ # is the whole lifecycle. There is deliberately no open/close pair: the SDK client is frozen
18
+ # after construction and holds no pool to release, so a lifecycle module here would be
19
+ # ceremony modelled on a protocol Rails does not have.
20
+ class DeliveryMethod
21
+ # @return [Hash{Symbol => Object}] the settings ActionMailer built this instance with.
22
+ attr_reader :settings
23
+
24
+ # @param settings [Hash{Symbol => Object}, nil] the `mailkube_settings` hash.
25
+ def initialize(settings = {})
26
+ @settings = settings || {}
27
+ end
28
+
29
+ # Deliver one message.
30
+ #
31
+ # @param message [Mail::Message] the message to send.
32
+ # @return [Mailkube::Email] the SDK's accepted-send result.
33
+ # @raise [DeliveryError] when the SDK reports any failure.
34
+ def deliver!(message)
35
+ fields = Payload.build(message)
36
+ # The three required keywords are named rather than left inside the splat. Steep cannot
37
+ # prove a Hash carries them, so a bare `**fields` would report them missing — and silencing
38
+ # that would silence the one check worth having here, which is that this gem still calls
39
+ # the SDK the way the SDK declares. A renamed keyword has to be a red build, not a
40
+ # production TypeError inside somebody's mailer.
41
+ client.emails.send(from: fields[:from], to: fields[:to], subject: fields[:subject],
42
+ **fields.except(:from, :to, :subject))
43
+ rescue Mailkube::Error => e
44
+ # Translated at the boundary, with the SDK error kept as `cause` so nothing is lost. See
45
+ # DeliveryError for why this matters even though `do_delivery` already rescues broadly.
46
+ raise DeliveryError, e.message
47
+ end
48
+
49
+ # The SDK client for this delivery, built once.
50
+ #
51
+ # Lazy rather than built in the constructor: ActionMailer instantiates a delivery method
52
+ # while wrapping a message even when the message is never delivered — `Mail::TestMailer`
53
+ # substitution, `deliver_later` handing off to a job, an interceptor that aborts — and a
54
+ # missing API key must not raise on any of those paths.
55
+ #
56
+ # @return [Mailkube::Client] the client.
57
+ def client = @client ||= Config.build_client(@settings)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ module Rails
5
+ # Raised when a delivery fails, wrapping the SDK error that caused it.
6
+ #
7
+ # **One class, and deliberately no subclasses.** Re-encoding the SDK's status taxonomy here
8
+ # would mean deciding what an HTTP 429 means, which the contract puts in the SDK's repository.
9
+ # The SDK's own exception is preserved as `cause`, so a caller that wants the category rescues
10
+ # it there, where the categories are defined once.
11
+ #
12
+ # ## Why translate at all
13
+ #
14
+ # Not for silent failure: `Mail::Message#do_delivery` rescues `StandardError` gated on
15
+ # `raise_delivery_errors`, so *any* exception class is already swallowed when an application
16
+ # asks for that. The reason is `deliver_later`. A queued delivery runs inside
17
+ # `ActionMailer::MailDeliveryJob`, and an application's `retry_on` / `discard_on` has to name a
18
+ # class. Naming an SDK class would make that application's retry policy break the day the SDK
19
+ # reorganized its hierarchy; naming this one means the SDK's taxonomy can change without it
20
+ # being a breaking change for consumers.
21
+ #
22
+ # This is the contract's "translate SDK errors into the framework's own error type" clause, in
23
+ # the one form Rails allows: ActionMailer ships no delivery-error class to translate into, so
24
+ # this gem defines the type consumers name.
25
+ #
26
+ # class OrderMailer < ApplicationMailer
27
+ # retry_on Mailkube::Rails::DeliveryError, wait: :polynomially_longer
28
+ # end
29
+ class DeliveryError < StandardError; end
30
+ end
31
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ module Rails
5
+ # The one place a `Mail::Message` becomes SDK send arguments.
6
+ #
7
+ # Every entry point calls {build}; nothing else maps a message. A gem with two entry points and
8
+ # two mappings has two behaviours, and only one of them is the one the tests cover.
9
+ #
10
+ # Scope is deliberately what ActionMailer's message type natively expresses: sender,
11
+ # recipients, subject, both bodies, attachments and custom headers. Tags, topics, templates,
12
+ # scheduling and idempotency keys are SDK features with no `Mail::Message` slot, so inventing a
13
+ # side channel for them here would create a second surface to document and test. Reach them by
14
+ # calling the SDK directly; the README says so.
15
+ module Payload
16
+ # Header names ActionMailer derives from the message itself.
17
+ #
18
+ # These are re-created by the API from the structured fields this mapping already sends, so
19
+ # forwarding them as "custom" headers would contradict what the API builds. `content-type` is
20
+ # the one that matters: it describes a MIME document that is never transmitted, because this
21
+ # gem hands over fields rather than a rendered message.
22
+ RESERVED_HEADERS = %w[
23
+ bcc cc content-transfer-encoding content-type date from
24
+ message-id mime-version reply-to subject to
25
+ ].freeze
26
+
27
+ # Convert a message into the keyword arguments for `client.emails.send`.
28
+ #
29
+ # @param message [Mail::Message] the message ActionMailer built.
30
+ # @return [Hash{Symbol => Object}] the SDK send keywords.
31
+ def self.build(message)
32
+ {
33
+ from: addresses(message, :from).first,
34
+ to: addresses(message, :to),
35
+ subject: message.subject.to_s,
36
+ html: body_for(message, "text/html"),
37
+ text: body_for(message, "text/plain"),
38
+ cc: presence(addresses(message, :cc)),
39
+ bcc: presence(addresses(message, :bcc)),
40
+ reply_to: presence(addresses(message, :reply_to)),
41
+ headers: presence(custom_headers(message)),
42
+ attachments: presence(attachments(message))
43
+ }.compact
44
+ end
45
+
46
+ # Render one address field as RFC strings, keeping display names.
47
+ #
48
+ # **Read through `message[field]`, not through `message.to_addrs`.** The `*_addrs` readers
49
+ # return BARE addresses, so a display name is already gone by the time this sees them and no
50
+ # amount of re-parsing brings it back. `Mail::Field#addrs` returns the parsed address objects,
51
+ # and `.format` renders each one as the application wrote it.
52
+ #
53
+ # `*_addrs` is also incomplete: `from_addrs`, `to_addrs`, `cc_addrs` and `bcc_addrs` exist but
54
+ # there is no `reply_to_addrs`, so that one reaches `method_missing` and raises. Going through
55
+ # the field object treats all five identically and cannot develop that asymmetry.
56
+ #
57
+ # @param message [Mail::Message] the message.
58
+ # @param field [Symbol] the header field name.
59
+ # @return [Array<String>] the formatted addresses, empty when the field is absent.
60
+ def self.addresses(message, field)
61
+ header = message[field]
62
+ return [] if header.nil?
63
+
64
+ # The annotation is what makes this checkable. `header` is untyped (the framework side is
65
+ # deliberately shallow in sig/vendor/), so without it the element type erases to `bot` and
66
+ # Steep rejects `&:format` while RuboCop insists on it — the two gates deadlock. Naming the
67
+ # type resolves both, and it is the one place this gem asserts what `Mail` hands back.
68
+ addrs = header.addrs #: Array[::Mail::Address]
69
+ addrs.map(&:format)
70
+ end
71
+
72
+ # Extract one body part as decoded text.
73
+ #
74
+ # `.decoded`, never `.raw_source`: a quoted-printable or base64 body would otherwise ship its
75
+ # transfer encoding to the API verbatim, and the recipient would read `=3D` where an equals
76
+ # sign belongs.
77
+ #
78
+ # @param message [Mail::Message] the message.
79
+ # @param mime_type [String] the part's MIME type.
80
+ # @return [String, nil] the decoded body, or nil when the message has no such part.
81
+ def self.body_for(message, mime_type)
82
+ part = part_for(message, mime_type)
83
+ return nil if part.nil?
84
+
85
+ # `.decoded` undoes the transfer encoding but hands back ASCII-8BIT bytes, with the part's
86
+ # charset recorded separately. Passing those to JSON either raises on invalid byte sequences
87
+ # or ships mojibake, so the bytes are reunited with their declared charset here. `charset`
88
+ # can be absent on a bare message, and UTF-8 is the only sane assumption when it is.
89
+ presence(transcode(part.body.decoded, part.charset))
90
+ end
91
+
92
+ # Reinterpret decoded bytes in the charset the part declared.
93
+ #
94
+ # @param body [String] the decoded bytes.
95
+ # @param charset [String, nil] the part's declared charset.
96
+ # @return [String] the body as UTF-8 text.
97
+ def self.transcode(body, charset)
98
+ body.dup.force_encoding(charset || "UTF-8").encode("UTF-8")
99
+ rescue ArgumentError, EncodingError
100
+ # An unknown or lying charset must not stop the send. The bytes go out as they arrived,
101
+ # which is what any non-transcoding mapping would have done anyway.
102
+ body
103
+ end
104
+
105
+ # Find the part carrying one MIME type, or nil.
106
+ #
107
+ # Split out of {body_for} so the branch has somewhere to return `untyped` from: written
108
+ # inline, the `if`/`elsif` has no `else` arm and Steep infers `bot` for the result, making the
109
+ # decode below unreachable in its eyes.
110
+ #
111
+ # @param message [Mail::Message] the message.
112
+ # @param mime_type [String] the part's MIME type.
113
+ # @return [Object, nil] the matching part, or nil.
114
+ def self.part_for(message, mime_type)
115
+ return message.all_parts.find { |part| part.mime_type == mime_type && !part.attachment? } if message.multipart?
116
+ return message if message.mime_type == mime_type
117
+ return message if mime_type == "text/plain" && message.mime_type.nil?
118
+
119
+ nil
120
+ end
121
+
122
+ # Convert the message's attachments into SDK attachments.
123
+ #
124
+ # `.body.decoded` hands over the original bytes; the SDK base64-encodes them for the wire.
125
+ # Encoding here would send them twice over.
126
+ #
127
+ # @param message [Mail::Message] the message.
128
+ # @return [Array<Mailkube::Attachment>] the attachments.
129
+ def self.attachments(message)
130
+ message.attachments.map do |attachment|
131
+ Mailkube::Attachment.new(
132
+ filename: attachment.filename.to_s,
133
+ content: attachment.body.decoded,
134
+ content_type: attachment.mime_type
135
+ )
136
+ end
137
+ end
138
+
139
+ # Collect the headers the application set itself.
140
+ #
141
+ # @param message [Mail::Message] the message.
142
+ # @return [Hash{String => String}] the custom headers.
143
+ def self.custom_headers(message)
144
+ headers = {} #: Hash[String, String]
145
+ message.header.fields.each do |field|
146
+ next if RESERVED_HEADERS.include?(field.name.to_s.downcase)
147
+
148
+ headers[field.name.to_s] = field.value.to_s
149
+ end
150
+ headers
151
+ end
152
+
153
+ # Treat an empty string, array or hash as absent.
154
+ #
155
+ # An unset field is **omitted** from the send rather than passed as an empty value: the SDK
156
+ # drops nils before serializing, so this is what keeps an empty `cc` off the wire instead of
157
+ # sending `"cc": []`.
158
+ #
159
+ # @param value [Object, nil] the mapped value.
160
+ # @return [Object, nil] the value, or nil when it is empty.
161
+ def self.presence(value)
162
+ return nil if value.nil?
163
+ return nil if value.respond_to?(:empty?) && value.empty?
164
+
165
+ value
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Mailkube
6
+ module Rails
7
+ # Registers the delivery method, and the gem's own options block, at boot.
8
+ #
9
+ # Note `::Rails::Railtie`, fully qualified. Inside this namespace the bare constant
10
+ # `Rails` is Mailkube::Rails, so `Rails::Railtie` would be a NameError at load. Every
11
+ # reference to the framework in this gem is written this way.
12
+ class Railtie < ::Rails::Railtie
13
+ # The gem's own settings block, so `config.mailkube.webhook_secret = ...` works in an
14
+ # initializer. `OrderedOptions` answers nil for anything unset, which is what lets
15
+ # {Config} read a key an application never assigned.
16
+ config.mailkube = ActiveSupport::OrderedOptions.new
17
+
18
+ # `before: "action_mailer.set_configs"` is load-bearing, not tidiness.
19
+ #
20
+ # `add_delivery_method` is what DEFINES the `mailkube_settings` class attribute
21
+ # (`action_mailer/delivery_methods.rb`: `class_attribute :"#{symbol}_settings"`). Rails then
22
+ # applies everything under `config.action_mailer` by calling those writers, with a bare
23
+ # `options.each { |k, v| send("#{k}=", v) }` at the end of `action_mailer.set_configs`.
24
+ #
25
+ # Register after that and an application setting `config.action_mailer.mailkube_settings`
26
+ # gets `NoMethodError: undefined method 'mailkube_settings='` at boot, which reads as a
27
+ # typo in their own config rather than as an ordering bug in this gem. `spec/naming_spec.rb`
28
+ # pins the declaration.
29
+ #
30
+ # The `on_load` hook is what defers the work until ActionMailer is actually loaded: naming
31
+ # the constant directly here would force it, and an application that does not use
32
+ # ActionMailer would pay for it at every boot.
33
+ initializer "mailkube-rails.add_delivery_method", before: "action_mailer.set_configs" do
34
+ ActiveSupport.on_load(:action_mailer) do
35
+ # steep:ignore:start
36
+ # `self` inside this block is `ActionMailer::Base`, which the framework substitutes at
37
+ # load time. No signature can express that, and modelling ActionMailer::Base in
38
+ # sig/vendor/ purely to satisfy one call would be a large stub for no checking value.
39
+ add_delivery_method :mailkube, Mailkube::Rails::DeliveryMethod
40
+ # steep:ignore:end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailkube
4
+ module Rails
5
+ # This gem's version, and the only place it is written.
6
+ #
7
+ # The gemspec reads it, and so does the User-Agent suffix, so a release cannot report one
8
+ # version to RubyGems and a different one to the API. It stays at 0.0.0 in the repository:
9
+ # semantic-release rewrites this line in the release runner and never commits it back. See
10
+ # `.rules/RELEASE.md`.
11
+ VERSION = "1.0.0"
12
+ end
13
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_controller"
4
+
5
+ require_relative "config"
6
+
7
+ module Mailkube
8
+ module Rails
9
+ # The inbound entry point: verify a delivery, then publish it.
10
+ #
11
+ # This file is **not** required by `lib/mailkube/rails.rb`. It is reached through the route an
12
+ # application writes, so a worker that only sends mail never loads ActionController.
13
+ #
14
+ # ## No engine, deliberately
15
+ #
16
+ # A mountable engine would force a path on every host application and drag `isolate_namespace`
17
+ # plus generator machinery along for one endpoint. Consumers write two lines instead, which
18
+ # also means they choose the path, the constraints and the middleware:
19
+ #
20
+ # require "mailkube/rails/webhooks_controller"
21
+ # post "/webhooks/mailkube", to: Mailkube::Rails::WebhooksController.action(:create)
22
+ #
23
+ # The `.action(:create)` form rather than the `"controller#action"` string: a gem's `lib/` is
24
+ # on `$LOAD_PATH` but is not an autoload path, so the string form would resolve to a constant
25
+ # Zeitwerk has never been told about and raise at the first request. The explicit `require` is
26
+ # what makes the constant exist, and is the reason this file can stay off the boot path.
27
+ #
28
+ # This differs from the Laravel integration, where a package registering a route is completely
29
+ # idiomatic and the route is registered for you behind an off-by-default setting.
30
+ #
31
+ # ## It contains no cryptography
32
+ #
33
+ # Verification is the SDK's, in one call. This class adapts Rails' request object to it and
34
+ # publishes the result. See `.rules/INTEGRATION_CONTRACT.md`.
35
+ class WebhooksController < ::ActionController::Base
36
+ # The notification this controller publishes on a verified delivery.
37
+ NOTIFICATION = "webhook.mailkube"
38
+
39
+ # A webhook POST is machine-to-machine and cannot carry a CSRF token, so the check is skipped
40
+ # rather than left to reject every delivery. The signature is what authenticates the request,
41
+ # and it is strictly stronger: CSRF protection proves a browser session, while the HMAC
42
+ # proves the sender holds the endpoint secret.
43
+ skip_forgery_protection
44
+
45
+ # Receive one webhook delivery.
46
+ #
47
+ # Answers 204 on success, 400 when verification fails, and 500 when no secret is configured.
48
+ #
49
+ # @return [void]
50
+ def create
51
+ secret = Config.webhook_secret(::Rails.application.config)
52
+ # 500, not 4xx. The sender is behaving correctly and the fault is entirely local, so this
53
+ # must read as an outage: the platform then keeps retrying, and the deliveries that arrive
54
+ # while the secret is missing are not silently discarded.
55
+ return head(:internal_server_error) if secret.nil?
56
+
57
+ event = Mailkube::Webhooks.verify(
58
+ payload: request.raw_post, headers: header_hash, secret: secret, **tolerance_argument
59
+ )
60
+ ActiveSupport::Notifications.instrument(NOTIFICATION, event: event)
61
+ head :no_content
62
+ rescue Mailkube::SignatureVerificationError, Mailkube::Error
63
+ # A malformed or unverifiable delivery is the sender's problem and a retry cannot fix it,
64
+ # so it is refused rather than retried. The reason is deliberately not reported: an
65
+ # endpoint that distinguishes "bad signature" from "stale timestamp" is an oracle.
66
+ head :bad_request
67
+ end
68
+
69
+ private
70
+
71
+ # The freshness window, as a splattable hash, or empty to accept the SDK's default.
72
+ #
73
+ # Only the optional argument travels in a splat: the three required ones are named at the
74
+ # call site so Steep checks them against the SDK's own signature. Omitted rather than passed
75
+ # as nil, because nil would be a value the SDK has to interpret rather than an absent one.
76
+ #
77
+ # @return [Hash{Symbol => Integer}] `{tolerance: n}`, or empty.
78
+ def tolerance_argument
79
+ tolerance = Config.webhook_tolerance(::Rails.application.config)
80
+ tolerance.nil? ? {} : { tolerance: tolerance }
81
+ end
82
+
83
+ # Convert the Rack environment into the plain Hash the SDK reads headers from.
84
+ #
85
+ # **`request.headers` cannot be passed through**, which is worth stating because it is the
86
+ # obvious thing to write and it fails twice over. `ActionDispatch::Http::Headers` includes
87
+ # `Enumerable` and nothing else, so it has no `transform_keys` and the SDK's normalization
88
+ # raises `NoMethodError` on it. And its `each` delegates to the Rack environment, so
89
+ # converting it to a Hash yields `HTTP_X_WEBHOOK_ID` rather than `X-Webhook-Id` — which
90
+ # downcases to something the SDK's lookup will never match, turning every delivery into a
91
+ # signature failure with no clue as to why.
92
+ #
93
+ # The conversion is generic rather than a list of the three headers the SDK reads today.
94
+ # Naming them would put a copy of the signature scheme's header set in this repository, and
95
+ # the copy would silently stop working the day the scheme grows a fourth.
96
+ #
97
+ # @return [Hash{String => String}] the request headers, in `x-webhook-id` form.
98
+ def header_hash
99
+ headers = {} #: Hash[String, String]
100
+ request.env.each do |name, value|
101
+ next unless name.is_a?(String) && name.start_with?("HTTP_") && value.is_a?(String)
102
+
103
+ headers[name.delete_prefix("HTTP_").downcase.tr("_", "-")] = value
104
+ end
105
+ headers
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mailkube"
4
+
5
+ require_relative "rails/version"
6
+ require_relative "rails/errors"
7
+ require_relative "rails/config"
8
+ require_relative "rails/payload"
9
+ require_relative "rails/delivery_method"
10
+ require_relative "rails/railtie"
11
+
12
+ module Mailkube
13
+ # ActionMailer delivery for mailkube, and an inbound webhook endpoint.
14
+ #
15
+ # This module is a **thin adapter**. The wire format, authentication, retry policy, error
16
+ # taxonomy and webhook signature scheme all belong to the `mailkube` gem; nothing here
17
+ # re-implements any of them. See `.rules/INTEGRATION_CONTRACT.md`.
18
+ #
19
+ # Requiring this file loads the delivery half only. {WebhooksController} is deliberately NOT
20
+ # required here: it subclasses `ActionController::Base`, and a worker process that only sends
21
+ # mail should not be made to load ActionController. Applications that receive webhooks reach it
22
+ # through the route they write, which autoloads it.
23
+ #
24
+ # ## The naming trap
25
+ #
26
+ # Inside this namespace the bare constant `Rails` resolves to **this module**, not to the
27
+ # framework. Every reference to the framework is therefore written `::Rails`, fully
28
+ # qualified. Ruby resolves the wrong one silently and the result is a `NoMethodError` a long way
29
+ # from the cause, so this is the most likely first bug in any change here. `spec/naming_spec.rb`
30
+ # pins it.
31
+ module Rails
32
+ end
33
+ end
@@ -0,0 +1,63 @@
1
+ # Signatures for this gem's public surface.
2
+ #
3
+ # These are checked two ways, and both are needed:
4
+ # `rbs validate` proves the signatures are internally coherent (every type they name exists).
5
+ # `steep check` proves the implementation matches them.
6
+ # `rbs validate` alone never loads `lib/`, so it would happily pass a `sig/` describing methods
7
+ # that do not exist, which is the exact drift this directory is here to prevent.
8
+ #
9
+ # The framework's own types are declared in `sig/vendor/`, which is NOT shipped in the gem. See
10
+ # the gemspec, and `.rules/RAILS_INTEGRATION.md`.
11
+ module Mailkube
12
+ module Rails
13
+ VERSION: String
14
+
15
+ class DeliveryError < StandardError
16
+ end
17
+
18
+ module Config
19
+ CLIENT_KEYS: Hash[Symbol, Symbol]
20
+
21
+ def self.build_client: (Hash[Symbol, untyped]?) -> Mailkube::Client
22
+ def self.client_kwargs: (Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
23
+ def self.user_agent_suffix: () -> String
24
+ def self.webhook_secret: (untyped) -> String?
25
+ def self.webhook_tolerance: (untyped) -> Integer?
26
+ def self.options: (untyped) -> untyped
27
+ end
28
+
29
+ module Payload
30
+ RESERVED_HEADERS: Array[String]
31
+
32
+ def self.build: (untyped) -> Hash[Symbol, untyped]
33
+ def self.addresses: (untyped, Symbol) -> Array[String]
34
+ def self.body_for: (untyped, String) -> String?
35
+ def self.transcode: (String, String?) -> String
36
+ def self.part_for: (untyped, String) -> untyped
37
+ def self.attachments: (untyped) -> Array[Mailkube::Attachment]
38
+ def self.custom_headers: (untyped) -> Hash[String, String]
39
+ def self.presence: (untyped) -> untyped
40
+ end
41
+
42
+ class DeliveryMethod
43
+ @client: Mailkube::Client?
44
+
45
+ attr_reader settings: Hash[Symbol, untyped]
46
+
47
+ def initialize: (?Hash[Symbol, untyped]?) -> void
48
+ def deliver!: (untyped) -> Mailkube::Email
49
+ def client: () -> Mailkube::Client
50
+ end
51
+
52
+ class Railtie < ::Rails::Railtie
53
+ end
54
+
55
+ class WebhooksController < ::ActionController::Base
56
+ NOTIFICATION: String
57
+
58
+ def create: () -> void
59
+ def tolerance_argument: () -> Hash[Symbol, Integer]
60
+ def header_hash: () -> Hash[String, String]
61
+ end
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailkube-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mailtactic, Corp.
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionmailer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.2.3
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 7.2.3
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: mailkube
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 1.1.0
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 1.1.0
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2'
52
+ - !ruby/object:Gem::Dependency
53
+ name: railties
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 7.2.3
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '9'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 7.2.3
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '9'
72
+ description: 'ActionMailer delivery method for mailkube. A thin adapter over the mailkube
73
+ gem: deliver through ActionMailer, receive webhooks as ActiveSupport notifications.'
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - LICENSE
79
+ - NOTICE
80
+ - README.md
81
+ - lib/mailkube/rails.rb
82
+ - lib/mailkube/rails/config.rb
83
+ - lib/mailkube/rails/delivery_method.rb
84
+ - lib/mailkube/rails/errors.rb
85
+ - lib/mailkube/rails/payload.rb
86
+ - lib/mailkube/rails/railtie.rb
87
+ - lib/mailkube/rails/version.rb
88
+ - lib/mailkube/rails/webhooks_controller.rb
89
+ - sig/mailkube/rails.rbs
90
+ homepage: https://github.com/mailkube/mailkube-rails
91
+ licenses:
92
+ - Apache-2.0
93
+ metadata:
94
+ source_code_uri: https://github.com/mailkube/mailkube-rails
95
+ changelog_uri: https://github.com/mailkube/mailkube-rails/releases
96
+ bug_tracker_uri: https://github.com/mailkube/mailkube-rails/issues
97
+ documentation_uri: https://rubydoc.info/gems/mailkube-rails
98
+ rubygems_mfa_required: 'true'
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '3.4'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.6.9
114
+ specification_version: 4
115
+ summary: ActionMailer delivery method for mailkube.
116
+ test_files: []