letter_opener_web-redis 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 +7 -0
- data/LICENSE +22 -0
- data/README.md +68 -0
- data/lib/letter_opener_web/redis_delivery_method.rb +21 -0
- data/lib/letter_opener_web/redis_letter.rb +186 -0
- data/lib/letter_opener_web/redis_message.rb +101 -0
- data/lib/letter_opener_web_redis.rb +31 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 210afa6ebf7862a38c9b5fd79e6d53f5331dcc91bafed3d386ef24e8a2b4125c
|
|
4
|
+
data.tar.gz: 5d0b6bd37962133afd3efc1d42208c035df1fa41702ffbfae84f32a0db21b921
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d1381185ae7a6dc594a0e3969c54ffab48386013c1ab5a2c4e0122fb876aeafd8011ffbc60e72d2240e86068affa2dd0f4b812e4bb4fbcec5ef7f07a35a42c60
|
|
7
|
+
data.tar.gz: cfd73ec500699cf764e2ac07881e1dc9b3def67717f62bfe0877db625e101a837eef4274352fe137fc8a3fc9a6c2d2e66d87363e7720fc9b36e7718eb3c9b408
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025 Letter Opener Web Redis Contributors
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# LetterOpenerWeb::Redis
|
|
2
|
+
|
|
3
|
+
Redis storage backend for [letter_opener_web](https://github.com/fgrehm/letter_opener_web).
|
|
4
|
+
|
|
5
|
+
This gem allows you to store email letters and attachments in Redis instead of the local filesystem, enabling multi-instance deployments and shared email viewing across application servers.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add these lines to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'letter_opener_web'
|
|
13
|
+
gem 'letter_opener_web-redis', require: 'letter_opener_web_redis'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
### 1. Configure Redis Connection
|
|
19
|
+
|
|
20
|
+
Set up your Redis connection in your Rails configuration file (e.g., `config/environments/development.rb`):
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Require the gem first
|
|
24
|
+
require 'letter_opener_web_redis'
|
|
25
|
+
|
|
26
|
+
LetterOpenerWeb.configure do |config|
|
|
27
|
+
# Use the Redis storage backend
|
|
28
|
+
config.letter_class = LetterOpenerWeb::RedisLetter
|
|
29
|
+
|
|
30
|
+
# Redis Configuration
|
|
31
|
+
config.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
32
|
+
config.redis_namespace = 'letter_opener' # optional, defaults to 'letter_opener'
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Configure ActionMailer
|
|
37
|
+
|
|
38
|
+
Update your ActionMailer configuration to use the Redis delivery method:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
config.action_mailer.delivery_method = :letter_opener_web_redis
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Storage Structure
|
|
45
|
+
|
|
46
|
+
Emails are stored in Redis with the following key structure:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
{namespace}:letters:ids # Set of all letter IDs
|
|
50
|
+
{namespace}:letter:{id}:metadata # JSON metadata (id, sent_at, attachments)
|
|
51
|
+
{namespace}:letter:{id}:plain # Plain text HTML
|
|
52
|
+
{namespace}:letter:{id}:rich # Rich HTML
|
|
53
|
+
{namespace}:letter:{id}:attachment:{filename} # Base64-encoded attachment content
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Where `{namespace}` defaults to `letter_opener` but can be customized via configuration.
|
|
57
|
+
|
|
58
|
+
## Memory Considerations
|
|
59
|
+
|
|
60
|
+
Since Redis stores all data in memory, be mindful of:
|
|
61
|
+
|
|
62
|
+
- **Email Size**: Large attachments will consume significant memory
|
|
63
|
+
- **Retention**: Consider implementing TTL (time-to-live) for old emails
|
|
64
|
+
- **Limits**: Monitor Redis memory usage and set appropriate limits
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'letter_opener'
|
|
4
|
+
|
|
5
|
+
module LetterOpenerWeb
|
|
6
|
+
class RedisDeliveryMethod < LetterOpener::DeliveryMethod
|
|
7
|
+
def deliver!(mail)
|
|
8
|
+
location = create_location
|
|
9
|
+
message = RedisMessage.new(mail, location: location)
|
|
10
|
+
message.render
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def create_location
|
|
16
|
+
location = File.join(settings[:location], "#{Time.now.to_i}_#{SecureRandom.hex(16)}")
|
|
17
|
+
FileUtils.mkdir_p(location)
|
|
18
|
+
location
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module LetterOpenerWeb
|
|
7
|
+
# Redis-based Letter implementation
|
|
8
|
+
#
|
|
9
|
+
# Stores and retrieves letters from Redis instead of the local filesystem.
|
|
10
|
+
# Letters are stored as Redis hashes with the following structure:
|
|
11
|
+
# - letter:{id}:plain - Plain text HTML
|
|
12
|
+
# - letter:{id}:rich - Rich HTML
|
|
13
|
+
# - letter:{id}:metadata - JSON with id, sent_at, attachments
|
|
14
|
+
# - letter:{id}:attachment:{filename} - Attachment content
|
|
15
|
+
# - letters:ids - Set of all letter IDs
|
|
16
|
+
class RedisLetter
|
|
17
|
+
include LetterOpenerWeb::LetterInterface
|
|
18
|
+
|
|
19
|
+
def self.redis
|
|
20
|
+
@redis ||= Redis.new(
|
|
21
|
+
url: LetterOpenerWeb.config.redis_url,
|
|
22
|
+
timeout: 5
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.key_prefix
|
|
27
|
+
namespace = LetterOpenerWeb.config.redis_namespace
|
|
28
|
+
namespace ? "#{namespace}:" : ''
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.search
|
|
32
|
+
letter_ids = redis.smembers("#{key_prefix}letters:ids")
|
|
33
|
+
|
|
34
|
+
letters = letter_ids.map do |id|
|
|
35
|
+
metadata = redis.get("#{key_prefix}letter:#{id}:metadata")
|
|
36
|
+
next unless metadata
|
|
37
|
+
|
|
38
|
+
data = JSON.parse(metadata)
|
|
39
|
+
new(id: data['id'], sent_at: Time.parse(data['sent_at']))
|
|
40
|
+
end.compact
|
|
41
|
+
|
|
42
|
+
letters.sort_by(&:sent_at).reverse
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.find(id)
|
|
46
|
+
new(id: id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.destroy_all
|
|
50
|
+
# Get all letter IDs
|
|
51
|
+
letter_ids = redis.smembers("#{key_prefix}letters:ids")
|
|
52
|
+
|
|
53
|
+
# Delete all keys associated with each letter
|
|
54
|
+
letter_ids.each do |id|
|
|
55
|
+
keys = redis.keys("#{key_prefix}letter:#{id}:*")
|
|
56
|
+
redis.del(*keys) if keys.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Clear the IDs set
|
|
60
|
+
redis.del("#{key_prefix}letters:ids")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(params)
|
|
64
|
+
@id = params.fetch(:id)
|
|
65
|
+
@sent_at = params[:sent_at] || Time.now
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def headers
|
|
69
|
+
html = read_file('rich') if file_exists?('rich')
|
|
70
|
+
html ||= read_file('plain')
|
|
71
|
+
|
|
72
|
+
return 'UNABLE TO PARSE HEADERS' if html.blank?
|
|
73
|
+
|
|
74
|
+
match_data = html.match(%r{<body>\s*<div[^>]+id="container">\s*<div[^>]+id="message_headers">\s*(<dl>.+</dl>)}m)
|
|
75
|
+
return remove_attachments_link(match_data[1]).html_safe if match_data && match_data[1].present?
|
|
76
|
+
|
|
77
|
+
'UNABLE TO PARSE HEADERS'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def plain_text
|
|
81
|
+
@plain_text ||= adjust_link_targets(read_file('plain'))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rich_text
|
|
85
|
+
@rich_text ||= adjust_link_targets(read_file('rich'))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def attachments
|
|
89
|
+
@attachments ||= begin
|
|
90
|
+
metadata = redis.get("#{key_prefix}letter:#{id}:metadata")
|
|
91
|
+
return {} unless metadata
|
|
92
|
+
|
|
93
|
+
data = JSON.parse(metadata)
|
|
94
|
+
data['attachments'] || {}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def delete
|
|
99
|
+
return unless valid?
|
|
100
|
+
|
|
101
|
+
keys = redis.keys("#{key_prefix}letter:#{id}:*")
|
|
102
|
+
redis.del(*keys) if keys.any?
|
|
103
|
+
redis.srem("#{key_prefix}letters:ids", id)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def valid?
|
|
107
|
+
file_exists?('rich') || file_exists?('plain')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def to_param
|
|
111
|
+
id
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def default_style
|
|
115
|
+
file_exists?('rich') ? 'rich' : 'plain'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def send_attachment(controller, filename)
|
|
119
|
+
return unless attachments.key?(filename)
|
|
120
|
+
|
|
121
|
+
encoded = redis.get("#{key_prefix}letter:#{id}:attachment:#{filename}")
|
|
122
|
+
return unless encoded
|
|
123
|
+
|
|
124
|
+
content = Base64.strict_decode64(encoded)
|
|
125
|
+
controller.send_data(content, filename: filename, type: 'application/octet-stream', disposition: 'inline')
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def redis
|
|
133
|
+
self.class.redis
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def key_prefix
|
|
137
|
+
self.class.key_prefix
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read_file(style)
|
|
141
|
+
redis.get("#{key_prefix}letter:#{id}:#{style}") || ''
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def file_exists?(style)
|
|
145
|
+
redis.exists("#{key_prefix}letter:#{id}:#{style}") == 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def remove_attachments_link(headers)
|
|
149
|
+
xml = REXML::Document.new(headers)
|
|
150
|
+
label_element = xml.root.elements.find { |e| e.get_text&.value&.match?(/attachments:/i) }
|
|
151
|
+
|
|
152
|
+
if label_element
|
|
153
|
+
xml.root.delete_element(label_element.next_element)
|
|
154
|
+
xml.root.delete_element(label_element)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
xml.to_s
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def adjust_link_targets(contents)
|
|
161
|
+
return contents if contents.blank?
|
|
162
|
+
|
|
163
|
+
contents.scan(%r{<a\s[^>]+>(?:.|\s)*?</a>}).each do |link|
|
|
164
|
+
fixed_link = fix_link_html(link)
|
|
165
|
+
xml = REXML::Document.new(fixed_link).root
|
|
166
|
+
next if xml.attributes['href'] =~ /(plain|rich).html/
|
|
167
|
+
|
|
168
|
+
xml.attributes['target'] = '_blank'
|
|
169
|
+
xml.add_text('') unless xml.text
|
|
170
|
+
contents.gsub!(link, xml.to_s)
|
|
171
|
+
end
|
|
172
|
+
contents
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def fix_link_html(link_html)
|
|
176
|
+
link_html.dup.tap do |fixed_link|
|
|
177
|
+
fixed_link.gsub!('<br>', '<br/>')
|
|
178
|
+
fixed_link.scan(/<img(?:[^>]+?)>/).each do |img|
|
|
179
|
+
fixed_img = img.dup
|
|
180
|
+
fixed_img.gsub!(/>$/, '/>') unless img =~ %r{/>$}
|
|
181
|
+
fixed_link.gsub!(img, fixed_img)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'letter_opener'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'base64'
|
|
6
|
+
|
|
7
|
+
module LetterOpenerWeb
|
|
8
|
+
# Extends LetterOpener::Message to store email content in Redis
|
|
9
|
+
#
|
|
10
|
+
# This class intercepts the message delivery and stores the generated
|
|
11
|
+
# HTML files and attachments in Redis instead of keeping them on local disk.
|
|
12
|
+
class RedisMessage < LetterOpener::Message
|
|
13
|
+
def render
|
|
14
|
+
# Call parent to generate the HTML files locally first
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
# Store in Redis
|
|
18
|
+
store_in_redis
|
|
19
|
+
|
|
20
|
+
# Clean up local files after storing
|
|
21
|
+
cleanup_local_files
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :location
|
|
27
|
+
|
|
28
|
+
def store_in_redis
|
|
29
|
+
store_html_files
|
|
30
|
+
store_attachments
|
|
31
|
+
store_metadata
|
|
32
|
+
register_letter_id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def store_html_files
|
|
36
|
+
%w[rich plain].each do |style|
|
|
37
|
+
file_path = "#{location}/#{style}.html"
|
|
38
|
+
next unless File.exist?(file_path)
|
|
39
|
+
|
|
40
|
+
content = File.read(file_path)
|
|
41
|
+
redis.set("#{key_prefix}letter:#{letter_id}:#{style}", content)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def store_attachments
|
|
46
|
+
attachments_dir = "#{location}/attachments"
|
|
47
|
+
return unless Dir.exist?(attachments_dir)
|
|
48
|
+
|
|
49
|
+
attachment_data = {}
|
|
50
|
+
|
|
51
|
+
Dir.glob("#{attachments_dir}/*").each do |file_path|
|
|
52
|
+
filename = File.basename(file_path)
|
|
53
|
+
|
|
54
|
+
# Store attachment content as base64
|
|
55
|
+
content = File.binread(file_path)
|
|
56
|
+
encoded = Base64.strict_encode64(content)
|
|
57
|
+
|
|
58
|
+
redis.set("#{key_prefix}letter:#{letter_id}:attachment:#{filename}", encoded)
|
|
59
|
+
|
|
60
|
+
# Track attachment in metadata
|
|
61
|
+
attachment_data[filename] = filename
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@attachment_data = attachment_data
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def store_metadata
|
|
68
|
+
metadata = {
|
|
69
|
+
id: letter_id,
|
|
70
|
+
sent_at: Time.now.iso8601,
|
|
71
|
+
attachments: @attachment_data || {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
redis.set("#{key_prefix}letter:#{letter_id}:metadata", metadata.to_json)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_letter_id
|
|
78
|
+
redis.sadd("#{key_prefix}letters:ids", letter_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cleanup_local_files
|
|
82
|
+
FileUtils.rm_rf(location)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def letter_id
|
|
86
|
+
@letter_id ||= File.basename(location)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def redis
|
|
90
|
+
@redis ||= Redis.new(
|
|
91
|
+
url: LetterOpenerWeb.config.redis_url,
|
|
92
|
+
timeout: 5
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def key_prefix
|
|
97
|
+
namespace = LetterOpenerWeb.config.redis_namespace
|
|
98
|
+
namespace ? "#{namespace}:" : ''
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'letter_opener_web'
|
|
4
|
+
require 'letter_opener_web/redis_letter'
|
|
5
|
+
require 'letter_opener_web/redis_message'
|
|
6
|
+
require 'letter_opener_web/redis_delivery_method'
|
|
7
|
+
|
|
8
|
+
# Add Redis-specific configuration to LetterOpenerWeb::Config immediately
|
|
9
|
+
LetterOpenerWeb::Config.class_eval do
|
|
10
|
+
attr_accessor :redis_url, :redis_namespace
|
|
11
|
+
|
|
12
|
+
# Set default Redis namespace
|
|
13
|
+
def redis_namespace
|
|
14
|
+
@redis_namespace ||= 'letter_opener'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module LetterOpenerWeb
|
|
19
|
+
module RedisStorage
|
|
20
|
+
class Railtie < Rails::Railtie
|
|
21
|
+
initializer 'letter_opener_web_redis.configure' do
|
|
22
|
+
# Register custom delivery method that uses RedisDeliveryMethod
|
|
23
|
+
ActionMailer::Base.add_delivery_method(
|
|
24
|
+
:letter_opener_web_redis,
|
|
25
|
+
LetterOpenerWeb::RedisDeliveryMethod,
|
|
26
|
+
location: Rails.root.join('tmp', 'letter_opener')
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: letter_opener_web-redis
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Adnan Ali
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: letter_opener_web
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: redis
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '4.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '4.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubocop
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.0'
|
|
69
|
+
description: Provides Redis-based storage for letter_opener_web, enabling multi-instance
|
|
70
|
+
deployments and shared email viewing across application servers.
|
|
71
|
+
email:
|
|
72
|
+
- adnan.ali@gmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- lib/letter_opener_web/redis_delivery_method.rb
|
|
80
|
+
- lib/letter_opener_web/redis_letter.rb
|
|
81
|
+
- lib/letter_opener_web/redis_message.rb
|
|
82
|
+
- lib/letter_opener_web_redis.rb
|
|
83
|
+
homepage: https://github.com/WizaCo/letter_opener_web-redis
|
|
84
|
+
licenses:
|
|
85
|
+
- MIT
|
|
86
|
+
metadata: {}
|
|
87
|
+
post_install_message:
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 3.1.0
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 3.3.26
|
|
103
|
+
signing_key:
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: Redis storage backend for letter_opener_web
|
|
106
|
+
test_files: []
|