duva-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/duva-rails.gemspec +26 -0
- data/lib/duva/delivery_method.rb +173 -0
- data/lib/duva/rails_version.rb +5 -0
- data/lib/duva/railtie.rb +16 -0
- data/lib/duva-rails.rb +11 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f1d0a7b608bf775dd3ec350b28df315969e99419004c724f389d29c3d4033a84
|
|
4
|
+
data.tar.gz: 30977e85fbe65ab750a0ca22d5cc5eacec48222c68e9091735fb7c6aeaf86f08
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ca4d394affd9cc2904606404ff19e1a8954501865e85262d564784967aae6aaea8df9f65b58b95f8e04a8c6e2484162cfc725bda4da32ac19004e03e7447a40d
|
|
7
|
+
data.tar.gz: 9aa14fa9618d458371956c0d8cc29c1e1f188b53a5af4e19899cff28a7268f7aa6ac747fc5bc3dedd8857737ae0fbcde87183d227d6d400fed8557cc93c5e047
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 9573-4562 Québec inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# duva-rails
|
|
2
|
+
|
|
3
|
+
Action Mailer delivery method for [Duva](https://duva.ca), the transactional email API hosted in Canada. Your mailers,
|
|
4
|
+
`deliver_later` jobs and Devise emails go out through Duva with no change to your mailer code.
|
|
5
|
+
|
|
6
|
+
Requires Ruby 3.3+ and Rails (Action Mailer) 7.2 to 8.x. Built on the official
|
|
7
|
+
[`duva-mail`](https://rubygems.org/gems/duva-mail) library.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "duva-rails"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
You also need a Duva API key and a verified domain ([documentation](https://duva.ca/en/docs)).
|
|
17
|
+
|
|
18
|
+
## Configure
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# config/environments/production.rb
|
|
22
|
+
config.action_mailer.delivery_method = :duva
|
|
23
|
+
config.action_mailer.duva_settings = {
|
|
24
|
+
api_key: Rails.application.credentials.dig(:duva, :api_key), # or ENV["DUVA_API_KEY"]
|
|
25
|
+
domain: "example.com" # or ENV["DUVA_DOMAIN"]
|
|
26
|
+
}
|
|
27
|
+
config.action_mailer.raise_delivery_errors = true
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# app/mailers/application_mailer.rb
|
|
32
|
+
default from: "Example <notifications@example.com>" # the address must belong to the Duva domain
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Optional settings: `base_url` (default `https://api.duva.ca`), `timeout` (seconds, default 10), `max_retries` (default 2).
|
|
36
|
+
`api_key` and `domain` fall back to the `DUVA_API_KEY` and `DUVA_DOMAIN` environment variables.
|
|
37
|
+
|
|
38
|
+
## What is sent
|
|
39
|
+
|
|
40
|
+
| Mail | Duva |
|
|
41
|
+
|---|---|
|
|
42
|
+
| From, To, Cc, Bcc, Reply-To | `from`, `to`, `cc`, `bcc`, `reply_to` (an address only in `smtp_envelope_to` is sent as Bcc) |
|
|
43
|
+
| `text` and `html` parts, or a single-part body | `text`, `html` |
|
|
44
|
+
| `attachments[...]`, `attachments.inline[...]` | `attachments` (inline ones keep their `content_id`) |
|
|
45
|
+
| `List-Unsubscribe`, `List-Unsubscribe-Post`, `List-Id`, `In-Reply-To`, `References`, `Auto-Submitted`, `Precedence`, `Importance`, `Feedback-ID`, and `X-*` headers | `headers` |
|
|
46
|
+
| `X-Duva-Tags`, `X-Duva-Metadata`, `X-Duva-Track`, `X-Idempotency-Key` | read by this gem, never sent (see below) |
|
|
47
|
+
|
|
48
|
+
Duva-specific options are set as headers in the mailer:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
def welcome(user)
|
|
52
|
+
headers["X-Duva-Tags"] = "welcome, onboarding" # tags
|
|
53
|
+
headers["X-Duva-Metadata"] = { user_id: user.id }.to_json # metadata (JSON object)
|
|
54
|
+
headers["X-Duva-Track"] = "opens, clicks" # tracking (needs an HTML part)
|
|
55
|
+
headers["X-Idempotency-Key"] = "welcome-#{user.id}" # see "Idempotency"
|
|
56
|
+
mail(to: user.email, subject: "Welcome")
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Notes:
|
|
61
|
+
|
|
62
|
+
- Other headers are not forwarded (Duva rejects them), nor are `X-Duva*`, `X-Kumo*`, `X-Tenant*` and `X-Campaign*`.
|
|
63
|
+
- A display name Duva refuses (it contains `@`, quotes, `<>`, control characters or an encoded word `=?…?=`) is dropped;
|
|
64
|
+
the address is kept.
|
|
65
|
+
- Delivery is asynchronous: a successful `deliver_now` means Duva accepted the message, not that it was delivered. Use
|
|
66
|
+
Duva's webhooks or events for the outcome.
|
|
67
|
+
- A Duva error (`Duva::Error` and its subclasses, from the `duva-mail` library) is raised from `deliver_now`, and
|
|
68
|
+
retried by Active Job in `deliver_later` according to your job configuration.
|
|
69
|
+
|
|
70
|
+
### Idempotency
|
|
71
|
+
|
|
72
|
+
A retried `deliver_later` job builds a new message, so a retry after a network timeout could send twice. Set a stable key
|
|
73
|
+
for it (an order ID, for example) with the `X-Idempotency-Key` header. Duva ignores a second message with the same key.
|
|
74
|
+
|
|
75
|
+
## Testing your application
|
|
76
|
+
|
|
77
|
+
Use `ActionMailer::Base.delivery_method = :test` in your tests as usual. To exercise this gem without a network, give it
|
|
78
|
+
the fake transport of the client library:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
transport = Duva::TestTransport.new { |_config, _spec| Duva::TestTransport.response("id" => "msg_...", "status" => "queued") }
|
|
82
|
+
ActionMailer::Base.duva_settings = { api_key: "dv_test", domain: "example.com", transport: transport }
|
|
83
|
+
# ... deliver ...
|
|
84
|
+
transport.requests.last.body # what would have been sent
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT.
|
data/duva-rails.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/duva/rails_version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "duva-rails"
|
|
7
|
+
spec.version = Duva::RAILS_VERSION
|
|
8
|
+
spec.summary = "Action Mailer delivery method for Duva, the transactional email API hosted in Canada."
|
|
9
|
+
spec.description = "Adds a :duva delivery method to Action Mailer: mailers, Active Job deliveries and " \
|
|
10
|
+
"Action Mailbox replies are sent through Duva. Built on the official duva-mail library."
|
|
11
|
+
spec.authors = ["9573-4562 Québec inc."]
|
|
12
|
+
spec.email = ["support@duva.ca"]
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.homepage = "https://duva.ca"
|
|
15
|
+
spec.metadata = {
|
|
16
|
+
"source_code_uri" => "https://github.com/duva-mail/duva-rails",
|
|
17
|
+
"documentation_uri" => "https://duva.ca/en/docs",
|
|
18
|
+
"changelog_uri" => "https://github.com/duva-mail/duva-rails/blob/main/CHANGELOG.md",
|
|
19
|
+
"rubygems_mfa_required" => "true"
|
|
20
|
+
}
|
|
21
|
+
spec.required_ruby_version = ">= 3.3"
|
|
22
|
+
spec.files = Dir.glob("lib/**/*.rb") + ["README.md", "LICENSE", "CHANGELOG.md", "duva-rails.gemspec"]
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
spec.add_dependency "actionmailer", ">= 7.2", "< 9"
|
|
25
|
+
spec.add_dependency "duva-mail", "~> 0.2"
|
|
26
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Duva
|
|
6
|
+
# Action Mailer delivery method: sends a Mail::Message through the Duva API.
|
|
7
|
+
#
|
|
8
|
+
# config.action_mailer.delivery_method = :duva
|
|
9
|
+
# config.action_mailer.duva_settings = { api_key: ENV["DUVA_API_KEY"], domain: "example.com" }
|
|
10
|
+
#
|
|
11
|
+
# Settings: `api_key`, `domain` (both fall back to DUVA_API_KEY and DUVA_DOMAIN), `base_url`, `timeout`,
|
|
12
|
+
# `max_retries`; `transport:` takes a Duva::TestTransport in your own tests.
|
|
13
|
+
#
|
|
14
|
+
# Duva refuses a few things Mail allows, and this class translates rather than failing late: a display
|
|
15
|
+
# name Duva would reject is dropped (the address is kept), and only the headers Duva accepts are
|
|
16
|
+
# forwarded. The `X-Duva-*` headers below are read here and never sent.
|
|
17
|
+
class DeliveryMethod
|
|
18
|
+
# Duva's `headers` allowlist (docs/api.md); every other `X-*` is allowed except the reserved prefixes.
|
|
19
|
+
ALLOWED_HEADERS = %w[
|
|
20
|
+
list-unsubscribe list-unsubscribe-post list-id in-reply-to references auto-submitted precedence
|
|
21
|
+
importance feedback-id
|
|
22
|
+
].freeze
|
|
23
|
+
RESERVED_PREFIXES = %w[x-kumo x-duva x-tenant x-campaign].freeze
|
|
24
|
+
# A name Duva refuses: `@`, quotes, angle brackets, backslash, control characters, encoded words.
|
|
25
|
+
UNSAFE_NAME = /[@"<>\\\u0000-\u001f\u007f]|=\?/
|
|
26
|
+
# A stable key so a retried job cannot send twice: read here, never forwarded.
|
|
27
|
+
IDEMPOTENCY_HEADER = "x-idempotency-key"
|
|
28
|
+
|
|
29
|
+
attr_accessor :settings
|
|
30
|
+
|
|
31
|
+
def initialize(settings = {})
|
|
32
|
+
@settings = settings
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Sends `mail`; returns the Duva::SendMessageResult (`id`, `status`, `replayed`) and sets the mail's
|
|
36
|
+
# Message-ID header to Duva's message id when it has none.
|
|
37
|
+
def deliver!(mail)
|
|
38
|
+
result = client.messages.deliver(**params(mail))
|
|
39
|
+
mail.message_id ||= result.id
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def client
|
|
46
|
+
@client ||= Duva::Client.new(**client_options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def client_options
|
|
50
|
+
options = settings.to_h.transform_keys(&:to_sym).slice(:api_key, :domain, :base_url, :timeout, :max_retries,
|
|
51
|
+
:transport)
|
|
52
|
+
options.compact
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def params(mail)
|
|
56
|
+
from = mail[:from]&.addrs&.first
|
|
57
|
+
raise ArgumentError, "Duva: the message has no From address" if from.nil?
|
|
58
|
+
|
|
59
|
+
{ from: format_address(from), to: addresses(mail[:to]), subject: mail.subject.to_s }
|
|
60
|
+
.merge(copies(mail), bodies(mail), reply_to(mail), from_headers(mail), attachments(mail))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def copies(mail)
|
|
64
|
+
{ cc: addresses(mail[:cc]), bcc: blind_copies(mail) }.reject { |_key, list| list.empty? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def reply_to(mail)
|
|
68
|
+
address = mail[:reply_to]&.addrs&.first
|
|
69
|
+
address ? { reply_to: format_address(address) } : {}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def attachments(mail)
|
|
73
|
+
list = mail.attachments.map { |attachment| to_attachment(attachment) }
|
|
74
|
+
list.empty? ? {} : { attachments: list }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def addresses(field)
|
|
78
|
+
(field&.addrs || []).map { |address| format_address(address) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Recipients only in the envelope (explicit smtp_envelope_to) and the Bcc header are blind copies.
|
|
82
|
+
def blind_copies(mail)
|
|
83
|
+
listed = (mail[:to]&.addrs.to_a + mail[:cc]&.addrs.to_a).map { |a| a.address.downcase }
|
|
84
|
+
from_header = mail[:bcc]&.addrs.to_a
|
|
85
|
+
from_envelope = mail.smtp_envelope_to.to_a.map { |address| Mail::Address.new(address) }
|
|
86
|
+
(from_header + from_envelope).uniq { |a| a.address.downcase }
|
|
87
|
+
.reject { |a| listed.include?(a.address.downcase) }
|
|
88
|
+
.map { |a| format_address(a) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `Name <address>`, or the bare address when Duva would refuse the name.
|
|
92
|
+
def format_address(address)
|
|
93
|
+
name = address.display_name.to_s
|
|
94
|
+
return address.address if name.empty? || UNSAFE_NAME.match?(name)
|
|
95
|
+
|
|
96
|
+
Duva::Address.format(address.address, name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def bodies(mail)
|
|
100
|
+
html = text = nil
|
|
101
|
+
if mail.multipart?
|
|
102
|
+
html = decode(mail.html_part)
|
|
103
|
+
text = decode(mail.text_part)
|
|
104
|
+
elsif mail.mime_type == "text/html"
|
|
105
|
+
html = decode(mail)
|
|
106
|
+
else
|
|
107
|
+
text = decode(mail)
|
|
108
|
+
end
|
|
109
|
+
{ html: html, text: text }.compact
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def decode(part)
|
|
113
|
+
return nil if part.nil?
|
|
114
|
+
|
|
115
|
+
body = part.body.decoded.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
116
|
+
body = body.encode(Encoding::UTF_8, part.charset || "UTF-8", invalid: :replace, undef: :replace) unless body.valid_encoding?
|
|
117
|
+
body.empty? ? nil : body
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Headers, tags, metadata, tracking and the idempotency key, all read from the Mail headers.
|
|
121
|
+
def from_headers(mail)
|
|
122
|
+
out = {}
|
|
123
|
+
headers = {}
|
|
124
|
+
mail.header.fields.each do |field|
|
|
125
|
+
name = field.name.to_s
|
|
126
|
+
lower = name.downcase
|
|
127
|
+
value = field.value.to_s
|
|
128
|
+
case lower
|
|
129
|
+
when IDEMPOTENCY_HEADER then out[:idempotency_key] = value
|
|
130
|
+
when "x-duva-tags" then out[:tags] = value.split(",").map(&:strip).reject(&:empty?)
|
|
131
|
+
when "x-duva-metadata" then out[:metadata] = parse_metadata(value)
|
|
132
|
+
when "x-duva-track" then out[:tracking] = parse_tracking(value)
|
|
133
|
+
else headers[name] = value if forwardable?(lower)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
out[:headers] = headers unless headers.empty?
|
|
137
|
+
out.reject { |_key, value| value.respond_to?(:empty?) && value.empty? }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# `X-Duva-Metadata: {"order_id": "42"}`. JSON, because Mail turns `_` into `-` in a header NAME.
|
|
141
|
+
def parse_metadata(value)
|
|
142
|
+
parsed = JSON.parse(value)
|
|
143
|
+
raise ArgumentError, "Duva: X-Duva-Metadata must be a JSON object" unless parsed.is_a?(Hash)
|
|
144
|
+
|
|
145
|
+
parsed.to_h { |key, entry| [key.to_s, entry.to_s] }
|
|
146
|
+
rescue JSON::ParserError
|
|
147
|
+
raise ArgumentError, "Duva: X-Duva-Metadata must be a JSON object"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# `X-Duva-Track: opens, clicks`
|
|
151
|
+
def parse_tracking(value)
|
|
152
|
+
wanted = value.split(",").map { |entry| entry.strip.downcase }
|
|
153
|
+
{ opens: wanted.include?("opens"), clicks: wanted.include?("clicks") }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def forwardable?(lower)
|
|
157
|
+
return true if ALLOWED_HEADERS.include?(lower)
|
|
158
|
+
|
|
159
|
+
lower.start_with?("x-") && RESERVED_PREFIXES.none? { |prefix| lower.start_with?(prefix) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def truthy?(value)
|
|
163
|
+
%w[true 1 yes on].include?(value.to_s.strip.downcase)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def to_attachment(attachment)
|
|
167
|
+
Duva::Attachments.from_bytes(
|
|
168
|
+
attachment.filename || "attachment", attachment.body.decoded,
|
|
169
|
+
content_type: attachment.mime_type, content_id: (attachment.cid if attachment.inline?)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/duva/railtie.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Duva
|
|
4
|
+
# Registers the delivery method once Action Mailer is loaded, so `config.action_mailer.delivery_method = :duva`
|
|
5
|
+
# and `config.action_mailer.duva_settings = {...}` work in config/environments/*.rb.
|
|
6
|
+
#
|
|
7
|
+
# BEFORE `action_mailer.set_configs`: that initializer applies every `config.action_mailer.*` option to
|
|
8
|
+
# ActionMailer::Base, and `duva_settings=` only exists once the delivery method is registered.
|
|
9
|
+
class Railtie < ::Rails::Railtie
|
|
10
|
+
initializer "duva.add_delivery_method", before: "action_mailer.set_configs" do
|
|
11
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
12
|
+
add_delivery_method :duva, Duva::DeliveryMethod
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/duva-rails.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_mailer"
|
|
4
|
+
require "duva"
|
|
5
|
+
require_relative "duva/rails_version"
|
|
6
|
+
require_relative "duva/delivery_method"
|
|
7
|
+
require_relative "duva/railtie" if defined?(Rails::Railtie)
|
|
8
|
+
|
|
9
|
+
# Registers the :duva delivery method with Action Mailer. Outside a Rails application (a plain
|
|
10
|
+
# Action Mailer script), `ActionMailer::Base.add_delivery_method` is called here directly.
|
|
11
|
+
ActionMailer::Base.add_delivery_method(:duva, Duva::DeliveryMethod) unless defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: duva-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- 9573-4562 Québec inc.
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: actionmailer
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.2'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.2'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: duva-mail
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.2'
|
|
47
|
+
description: 'Adds a :duva delivery method to Action Mailer: mailers, Active Job deliveries
|
|
48
|
+
and Action Mailbox replies are sent through Duva. Built on the official duva-mail
|
|
49
|
+
library.'
|
|
50
|
+
email:
|
|
51
|
+
- support@duva.ca
|
|
52
|
+
executables: []
|
|
53
|
+
extensions: []
|
|
54
|
+
extra_rdoc_files: []
|
|
55
|
+
files:
|
|
56
|
+
- CHANGELOG.md
|
|
57
|
+
- LICENSE
|
|
58
|
+
- README.md
|
|
59
|
+
- duva-rails.gemspec
|
|
60
|
+
- lib/duva-rails.rb
|
|
61
|
+
- lib/duva/delivery_method.rb
|
|
62
|
+
- lib/duva/rails_version.rb
|
|
63
|
+
- lib/duva/railtie.rb
|
|
64
|
+
homepage: https://duva.ca
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
source_code_uri: https://github.com/duva-mail/duva-rails
|
|
69
|
+
documentation_uri: https://duva.ca/en/docs
|
|
70
|
+
changelog_uri: https://github.com/duva-mail/duva-rails/blob/main/CHANGELOG.md
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
72
|
+
post_install_message:
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.3'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 3.5.22
|
|
88
|
+
signing_key:
|
|
89
|
+
specification_version: 4
|
|
90
|
+
summary: Action Mailer delivery method for Duva, the transactional email API hosted
|
|
91
|
+
in Canada.
|
|
92
|
+
test_files: []
|