gemstack-mail 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.txt +21 -0
- data/README.md +28 -0
- data/lib/gemstack/mail/delivery_job.rb +20 -0
- data/lib/gemstack/mail/testing.rb +33 -0
- data/lib/gemstack/mail.rb +232 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f307a2cbc3114db71b11ebfd91308197c64b1ac2e62ff5de5c228eef8aa55afd
|
|
4
|
+
data.tar.gz: 1154fb801ecab736d9c38f47fc66c3f35038b7b16139c6ae40e2cc603a6e77bb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 377bce3b6a5d4d460e595304fe2d68aba4e4bd847c307fdd7082b7ef91473183baaea175b61185e422851d5f06b1cbb222f546bdcc27fa7a20bedfd30efe0a01
|
|
7
|
+
data.tar.gz: d2bf854eaacf75de80a3b1c32629b9277e33755e6097bac4757712024100748f3f08251162e4fd29db6e1abb1bb058a982c7740afcb4bc6bfd24bf3148ce8cc8
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shoaib Malik
|
|
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,28 @@
|
|
|
1
|
+
# gemstack-mail
|
|
2
|
+
|
|
3
|
+
GemStack mail: mailers, templates, SMTP/log/test delivery, deliver_later via jobs.
|
|
4
|
+
|
|
5
|
+
Part of [GemStack](https://github.com/gemstack-rb/gemstack), a modular Ruby API framework for Next.js
|
|
6
|
+
applications. All GemStack gems are developed together in that repository and released with the same
|
|
7
|
+
version.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Optional module — `gemstack add auth`, or add it to the Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "gemstack-mail", "~> 0.1"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Documentation
|
|
18
|
+
|
|
19
|
+
- [Guide](https://github.com/gemstack-rb/gemstack/blob/main/docs/mail.md)
|
|
20
|
+
- [All guides](https://github.com/gemstack-rb/gemstack/tree/main/docs) ·
|
|
21
|
+
[Architecture](https://github.com/gemstack-rb/gemstack/blob/main/ARCHITECTURE.md)
|
|
22
|
+
|
|
23
|
+
Source, issues and pull requests: [gemstack-rb/gemstack](https://github.com/gemstack-rb/gemstack)
|
|
24
|
+
(this gem lives in `gems/gemstack-mail`).
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gemstack/jobs"
|
|
4
|
+
|
|
5
|
+
module GemStack
|
|
6
|
+
module Mail
|
|
7
|
+
# Delivers a mailer action in the background (Delivery#deliver_later).
|
|
8
|
+
# Retried like any job; SMTP errors are transient more often than not.
|
|
9
|
+
class DeliveryJob < GemStack::Job
|
|
10
|
+
def self.queue(name = nil) = name ? super : (@queue || Mail.config.queue)
|
|
11
|
+
|
|
12
|
+
def perform(mailer_name, action, args)
|
|
13
|
+
mailer = Object.const_get(mailer_name)
|
|
14
|
+
raise ArgumentError, "#{mailer_name} is not a GemStack::Mailer" unless mailer.is_a?(Class) && mailer < Mailer
|
|
15
|
+
|
|
16
|
+
Mailer::Delivery.new(mailer, action, args).deliver_now
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gemstack/mail"
|
|
4
|
+
|
|
5
|
+
module GemStack
|
|
6
|
+
module Mail
|
|
7
|
+
# Test helpers (included into GemStack::TestCase by `gemstack add auth`):
|
|
8
|
+
#
|
|
9
|
+
# assert_emails(1) { post_json "/api/auth/password/forgot", { email: user.email } }
|
|
10
|
+
# assert_equal [user.email], last_email.to
|
|
11
|
+
module Testing
|
|
12
|
+
def before_setup
|
|
13
|
+
super
|
|
14
|
+
Mail.deliveries.clear
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deliveries = Mail.deliveries
|
|
18
|
+
def last_email = Mail.deliveries.last
|
|
19
|
+
|
|
20
|
+
def assert_emails(count, &block)
|
|
21
|
+
before = Mail.deliveries.size
|
|
22
|
+
adapter = defined?(GemStack::Jobs) && GemStack::Jobs.adapter
|
|
23
|
+
adapter = nil unless adapter.respond_to?(:perform_enqueued)
|
|
24
|
+
queued = adapter ? adapter.enqueued.map { |job| job["id"] } : []
|
|
25
|
+
block&.call
|
|
26
|
+
# deliver_later mail sent inside the block counts too.
|
|
27
|
+
adapter&.perform_enqueued(only: ["GemStack::Mail::DeliveryJob"], except_ids: queued)
|
|
28
|
+
assert_equal count, Mail.deliveries.size - before,
|
|
29
|
+
"Expected #{count} email(s), got #{Mail.deliveries.size - before}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mail"
|
|
4
|
+
require "erubi"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "gemstack/core"
|
|
7
|
+
|
|
8
|
+
module GemStack
|
|
9
|
+
# Email (DECISIONS D-047).
|
|
10
|
+
#
|
|
11
|
+
# class AccountMailer < GemStack::Mailer
|
|
12
|
+
# default from: "Shop <hello@shop.test>"
|
|
13
|
+
#
|
|
14
|
+
# def welcome(user_id)
|
|
15
|
+
# @user = User.find(user_id)
|
|
16
|
+
# mail to: @user.email, subject: "Welcome!" # templates/account_mailer/welcome.{text,html}.erb
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# AccountMailer.welcome(user.id).deliver_later # through GemStack::Jobs (pass ids)
|
|
21
|
+
# AccountMailer.welcome(user.id).deliver_now
|
|
22
|
+
module Mail
|
|
23
|
+
class Config < Settings
|
|
24
|
+
# :smtp (production), :log (development: logged and saved to tmp/mail),
|
|
25
|
+
# :test (captured in GemStack::Mail.deliveries), or an object with #deliver(message).
|
|
26
|
+
setting :delivery, default: lambda {
|
|
27
|
+
if GemStack.env.test? then :test
|
|
28
|
+
elsif GemStack.env.development? then :log
|
|
29
|
+
else :smtp
|
|
30
|
+
end
|
|
31
|
+
}
|
|
32
|
+
# smtp://user:password@smtp.example.com:587 (STARTTLS) or smtps://…:465
|
|
33
|
+
setting :smtp_url, default: -> { ENV.fetch("SMTP_URL", nil) }
|
|
34
|
+
setting :default_from, default: -> { ENV.fetch("MAIL_FROM", "#{GemStack.config.name} <no-reply@localhost>") }
|
|
35
|
+
setting :templates_path, default: "app/mailers/templates"
|
|
36
|
+
setting :preview_dir, default: "tmp/mail"
|
|
37
|
+
# Queue used by deliver_later.
|
|
38
|
+
setting :queue, default: "mailers"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class DeliveryError < Error; end
|
|
42
|
+
|
|
43
|
+
# Loaded on first use, also by a jobs worker resolving the queued class name.
|
|
44
|
+
autoload :DeliveryJob, "gemstack/mail/delivery_job"
|
|
45
|
+
|
|
46
|
+
@deliveries = []
|
|
47
|
+
@mutex = Mutex.new
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
def config = GemStack.config.mail
|
|
51
|
+
|
|
52
|
+
# Messages delivered with the :test method.
|
|
53
|
+
attr_reader :deliveries
|
|
54
|
+
|
|
55
|
+
def deliver(message)
|
|
56
|
+
method = config.delivery
|
|
57
|
+
case method
|
|
58
|
+
when :test, "test" then @mutex.synchronize { @deliveries << message }
|
|
59
|
+
when :log, "log" then deliver_to_log(message)
|
|
60
|
+
when :smtp, "smtp" then deliver_smtp(message)
|
|
61
|
+
else
|
|
62
|
+
unless method.respond_to?(:deliver)
|
|
63
|
+
raise ConfigurationError,
|
|
64
|
+
"a mail delivery method must respond to #deliver"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
method.deliver(message)
|
|
68
|
+
end
|
|
69
|
+
GemStack.logger.info("mail.delivered", to: Array(message.to).join(","), subject: message.subject,
|
|
70
|
+
via: method.is_a?(Symbol) ? method : method.class.name)
|
|
71
|
+
message
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def smtp_settings(url = config.smtp_url)
|
|
75
|
+
if url.to_s.empty?
|
|
76
|
+
raise ConfigurationError,
|
|
77
|
+
"set SMTP_URL (e.g. smtp://user:pass@smtp.example.com:587) to send mail"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
uri = URI.parse(url)
|
|
81
|
+
{
|
|
82
|
+
address: uri.host, port: uri.port || (uri.scheme == "smtps" ? 465 : 587),
|
|
83
|
+
user_name: uri.user && URI.decode_www_form_component(uri.user),
|
|
84
|
+
password: uri.password && URI.decode_www_form_component(uri.password),
|
|
85
|
+
authentication: uri.user ? :plain : nil, tls: uri.scheme == "smtps",
|
|
86
|
+
enable_starttls_auto: uri.scheme != "smtps", open_timeout: 5, read_timeout: 10
|
|
87
|
+
}.compact
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def deliver_smtp(message)
|
|
93
|
+
message.delivery_method(:smtp, smtp_settings)
|
|
94
|
+
message.deliver
|
|
95
|
+
rescue Net::SMTPError, SocketError, SystemCallError, Timeout::Error, OpenSSL::SSL::SSLError => e
|
|
96
|
+
raise DeliveryError, "SMTP delivery failed: #{e.class}: #{e.message}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Development: a log line plus a copy on disk (.eml, and .html to open in a browser).
|
|
100
|
+
def deliver_to_log(message)
|
|
101
|
+
dir = File.expand_path(config.preview_dir, GemStack.config.root)
|
|
102
|
+
FileUtils.mkdir_p(dir)
|
|
103
|
+
slug = message.subject.to_s.downcase.gsub(/[^a-z0-9]+/, "-").delete_prefix("-")[0, 40]
|
|
104
|
+
base = File.join(dir, "#{Time.now.strftime("%Y%m%d-%H%M%S-%L")}-#{slug}")
|
|
105
|
+
File.write("#{base}.eml", message.to_s)
|
|
106
|
+
html = message.html_part&.decoded
|
|
107
|
+
File.write("#{base}.html", html) if html
|
|
108
|
+
GemStack.logger.info("mail saved", to: Array(message.to).join(","), subject: message.subject,
|
|
109
|
+
file: "#{base}#{".html" if html}")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Base class for mailers. Public instance methods are mail "actions";
|
|
115
|
+
# calling one on the class returns a Delivery. An action that returns
|
|
116
|
+
# without calling `mail` sends nothing (e.g. the user was deleted since).
|
|
117
|
+
class Mailer
|
|
118
|
+
# Rendered as the value of @variables set in the action.
|
|
119
|
+
class TemplateContext
|
|
120
|
+
def initialize(mailer)
|
|
121
|
+
mailer.instance_variables.each { |ivar| instance_variable_set(ivar, mailer.instance_variable_get(ivar)) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def render(source, escape:) = instance_eval(Erubi::Engine.new(source, escape: escape).src)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A message waiting to be delivered.
|
|
128
|
+
class Delivery
|
|
129
|
+
attr_reader :mailer_class, :action, :args
|
|
130
|
+
|
|
131
|
+
def initialize(mailer_class, action, args)
|
|
132
|
+
@mailer_class = mailer_class
|
|
133
|
+
@action = action
|
|
134
|
+
@args = args
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def message
|
|
138
|
+
return @message if defined?(@message)
|
|
139
|
+
|
|
140
|
+
@message = mailer_class.new.build(action, args)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# nil when the action decided not to send (it returned without calling mail).
|
|
144
|
+
def deliver_now
|
|
145
|
+
msg = message
|
|
146
|
+
msg && Mail.deliver(msg)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Delivers from a background job (arguments must be JSON values — pass ids).
|
|
150
|
+
def deliver_later(wait: nil)
|
|
151
|
+
require "gemstack/jobs"
|
|
152
|
+
job = wait ? Mail::DeliveryJob.set(wait: wait) : Mail::DeliveryJob
|
|
153
|
+
job.perform_later(mailer_class.name, action.to_s, args)
|
|
154
|
+
rescue LoadError
|
|
155
|
+
raise ConfigurationError, "deliver_later needs gemstack-jobs; add it to the Gemfile or use deliver_now"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class << self
|
|
160
|
+
def default(**headers) = defaults.merge!(headers.transform_keys(&:to_sym))
|
|
161
|
+
def defaults = @defaults ||= superclass.respond_to?(:defaults) ? superclass.defaults.dup : {}
|
|
162
|
+
|
|
163
|
+
def actions
|
|
164
|
+
(public_instance_methods(true) - Mailer.public_instance_methods(true)).map(&:to_s)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def respond_to_missing?(name, include_private = false) = actions.include?(name.to_s) || super
|
|
168
|
+
|
|
169
|
+
def method_missing(name, *args)
|
|
170
|
+
return super unless actions.include?(name.to_s)
|
|
171
|
+
|
|
172
|
+
Delivery.new(self, name.to_s, args)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build(action, args)
|
|
177
|
+
@action = action
|
|
178
|
+
public_send(action, *args)
|
|
179
|
+
GemStack.logger.debug("mail skipped", mailer: self.class.name, action: action) unless @message
|
|
180
|
+
@message
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Builds the message. Bodies come from text:/html: or, when omitted, from
|
|
184
|
+
# templates: <templates_path>/<mailer>/<action>.text.erb and .html.erb
|
|
185
|
+
# (HTML templates escape <%= %> output; use <%== %> for trusted markup).
|
|
186
|
+
def mail(to:, subject:, text: nil, html: nil, from: nil, cc: nil, bcc: nil, reply_to: nil, template: @action)
|
|
187
|
+
headers = self.class.defaults
|
|
188
|
+
text ||= render_template(template, "text", escape: false)
|
|
189
|
+
html ||= render_template(template, "html", escape: true)
|
|
190
|
+
raise Error, "#{self.class.name}##{@action}: no body (give text:/html: or add a template)" unless text || html
|
|
191
|
+
|
|
192
|
+
@message = build_message(to: to, subject: subject, text: text, html: html,
|
|
193
|
+
from: from || headers[:from] || Mail.config.default_from, cc: cc, bcc: bcc,
|
|
194
|
+
reply_to: reply_to || headers[:reply_to])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def render_template(name, format, escape:)
|
|
200
|
+
path = File.join(GemStack.config.root, Mail.config.templates_path, Inflector.underscore(self.class.name),
|
|
201
|
+
"#{name}.#{format}.erb")
|
|
202
|
+
return nil unless File.file?(path)
|
|
203
|
+
|
|
204
|
+
TemplateContext.new(self).render(File.read(path), escape: escape)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def build_message(to:, subject:, text:, html:, from:, cc:, bcc:, reply_to:)
|
|
208
|
+
message = ::Mail.new
|
|
209
|
+
message.from = from
|
|
210
|
+
message.to = to
|
|
211
|
+
message.cc = cc if cc
|
|
212
|
+
message.bcc = bcc if bcc
|
|
213
|
+
message.reply_to = reply_to if reply_to
|
|
214
|
+
message.subject = subject
|
|
215
|
+
if text
|
|
216
|
+
message.text_part = ::Mail::Part.new do
|
|
217
|
+
body text
|
|
218
|
+
content_type "text/plain; charset=UTF-8"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
if html
|
|
222
|
+
message.html_part = ::Mail::Part.new do
|
|
223
|
+
body html
|
|
224
|
+
content_type "text/html; charset=UTF-8"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
message
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
GemStack::Config.namespace(:mail, GemStack::Mail::Config)
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemstack-mail
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Shoaib Malik
|
|
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: erubi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.13'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: gemstack-core
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.1.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: mail
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.9'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.9'
|
|
54
|
+
email:
|
|
55
|
+
- gemstack26@gmail.com
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- CHANGELOG.md
|
|
61
|
+
- LICENSE.txt
|
|
62
|
+
- README.md
|
|
63
|
+
- lib/gemstack/mail.rb
|
|
64
|
+
- lib/gemstack/mail/delivery_job.rb
|
|
65
|
+
- lib/gemstack/mail/testing.rb
|
|
66
|
+
homepage: https://github.com/gemstack-rb/gemstack
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
source_code_uri: https://github.com/gemstack-rb/gemstack/tree/main/gems/gemstack-mail
|
|
72
|
+
changelog_uri: https://github.com/gemstack-rb/gemstack/blob/main/gems/gemstack-mail/CHANGELOG.md
|
|
73
|
+
bug_tracker_uri: https://github.com/gemstack-rb/gemstack/issues
|
|
74
|
+
documentation_uri: https://github.com/gemstack-rb/gemstack/tree/main/docs
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '4.0'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.20
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: 'GemStack mail: mailers, templates, SMTP/log/test delivery, deliver_later
|
|
92
|
+
via jobs'
|
|
93
|
+
test_files: []
|