letter_opener_web-s3 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 +21 -0
- data/README.md +74 -0
- data/lib/letter_opener_web/s3_delivery_method.rb +21 -0
- data/lib/letter_opener_web/s3_letter.rb +234 -0
- data/lib/letter_opener_web/s3_message.rb +100 -0
- data/lib/letter_opener_web_s3.rb +34 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9f8303ddd8e04e9bfc35eb37bff20c0c641e1bf1b3886dfecb730653848c144e
|
|
4
|
+
data.tar.gz: cca476d21c279b3dd9fe566bc72b71a5328e91c4d77961b44157f8c81db17bb8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c68324796054e9a89e592797a79f07ad1e4d66b2ee01a16b3a3068d7d3f8af0c6bd2449eda2bb7e2ae97d516fd40f7bd119e473f284fb4ba98a881c1d7efca6
|
|
7
|
+
data.tar.gz: 8bd2d3ea6f70b94a1e28d1d18b1241790585a5c043e88e61c6489de8eb3d00b484fcdbfc78a85a000444cd2a92d2033169dd28eb8af3b2c929ca4de7453b2f0d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Letter Opener Web Contributors
|
|
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,74 @@
|
|
|
1
|
+
# LetterOpenerWeb::S3
|
|
2
|
+
|
|
3
|
+
Amazon S3 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 on Amazon S3 instead of the local filesystem, enabling multi-instance deployments on platforms like Heroku, Kubernetes, or any containerized environment.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'letter_opener_web'
|
|
13
|
+
gem 'letter_opener_web-s3', require: 'letter_opener_web_s3'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
And then execute:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
### 1. Configure AWS Credentials
|
|
25
|
+
|
|
26
|
+
Set up your AWS credentials and S3 bucket in your Rails configuration file (e.g., `config/environments/development.rb`):
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Require the gem first
|
|
30
|
+
require 'letter_opener_web_s3'
|
|
31
|
+
|
|
32
|
+
LetterOpenerWeb.configure do |config|
|
|
33
|
+
# Use the S3 storage backend
|
|
34
|
+
config.letter_class = LetterOpenerWeb::S3Letter
|
|
35
|
+
|
|
36
|
+
# AWS Configuration
|
|
37
|
+
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
|
38
|
+
config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
|
39
|
+
config.aws_region = 'us-east-1'
|
|
40
|
+
config.aws_bucket = 'my-app-development-emails'
|
|
41
|
+
config.aws_bucket_prefix = 'letters' # Optional, defaults to 'letters'
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Configure ActionMailer
|
|
46
|
+
|
|
47
|
+
Update your ActionMailer configuration to use the S3 delivery method:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
config.action_mailer.delivery_method = :letter_opener_web_s3
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Storage Structure
|
|
54
|
+
|
|
55
|
+
Emails are stored in S3 with the following structure (using default prefix `letters`):
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
YOUR-BUCKET-NAME/
|
|
59
|
+
└── letters/ # Configurable via aws_bucket_prefix
|
|
60
|
+
├── {timestamp_id_1}/
|
|
61
|
+
│ ├── plain.html
|
|
62
|
+
│ ├── rich.html
|
|
63
|
+
│ └── attachments/
|
|
64
|
+
│ ├── file1.pdf
|
|
65
|
+
│ └── file2.jpg
|
|
66
|
+
└── {timestamp_id_2}/
|
|
67
|
+
└── ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You can customize the prefix by setting `config.aws_bucket_prefix` to organize emails under different paths in your bucket.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
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 S3DeliveryMethod < LetterOpener::DeliveryMethod
|
|
7
|
+
def deliver!(mail)
|
|
8
|
+
location = create_location
|
|
9
|
+
message = S3Message.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,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aws-sdk-s3'
|
|
4
|
+
|
|
5
|
+
module LetterOpenerWeb
|
|
6
|
+
# S3-based Letter implementation
|
|
7
|
+
#
|
|
8
|
+
# Stores and retrieves letters from Amazon S3 instead of the local filesystem.
|
|
9
|
+
# This enables multi-instance deployments (Heroku, containers, etc.)
|
|
10
|
+
class S3Letter
|
|
11
|
+
include LetterOpenerWeb::LetterInterface
|
|
12
|
+
|
|
13
|
+
def self.aws_client
|
|
14
|
+
@aws_client ||= Aws::S3::Client.new(
|
|
15
|
+
access_key_id: LetterOpenerWeb.config.aws_access_key_id,
|
|
16
|
+
secret_access_key: LetterOpenerWeb.config.aws_secret_access_key,
|
|
17
|
+
region: LetterOpenerWeb.config.aws_region
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.bucket
|
|
22
|
+
LetterOpenerWeb.config.aws_bucket
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.bucket_prefix
|
|
26
|
+
LetterOpenerWeb.config.aws_bucket_prefix
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.search
|
|
30
|
+
response = aws_client.list_objects_v2(
|
|
31
|
+
bucket: bucket,
|
|
32
|
+
delimiter: '/',
|
|
33
|
+
prefix: "#{bucket_prefix}/"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
letters = response.common_prefixes.map do |prefix|
|
|
37
|
+
id = prefix.prefix.delete_prefix("#{bucket_prefix}/").delete_suffix('/')
|
|
38
|
+
|
|
39
|
+
# Get the last modified time from one of the files in this letter
|
|
40
|
+
obj = aws_client.list_objects_v2(
|
|
41
|
+
bucket: bucket,
|
|
42
|
+
prefix: prefix.prefix,
|
|
43
|
+
max_keys: 1
|
|
44
|
+
).contents.first
|
|
45
|
+
|
|
46
|
+
new(id: id, sent_at: obj&.last_modified || Time.now)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
letters.sort_by(&:sent_at).reverse
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.find(id)
|
|
53
|
+
new(id: id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.destroy_all
|
|
57
|
+
response = aws_client.list_objects_v2(
|
|
58
|
+
bucket: bucket,
|
|
59
|
+
prefix: "#{bucket_prefix}/"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return if response.contents.empty?
|
|
63
|
+
|
|
64
|
+
objects_to_delete = response.contents.map { |obj| { key: obj.key } }
|
|
65
|
+
|
|
66
|
+
aws_client.delete_objects(
|
|
67
|
+
bucket: bucket,
|
|
68
|
+
delete: { objects: objects_to_delete }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
attr_reader :id, :sent_at
|
|
73
|
+
|
|
74
|
+
def initialize(params)
|
|
75
|
+
@id = params.fetch(:id)
|
|
76
|
+
@sent_at = params[:sent_at]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def headers
|
|
80
|
+
html = read_file('rich') if file_exists?('rich')
|
|
81
|
+
html ||= read_file('plain')
|
|
82
|
+
|
|
83
|
+
return 'UNABLE TO PARSE HEADERS' if html.blank?
|
|
84
|
+
|
|
85
|
+
match_data = html.match(%r{<body>\s*<div[^>]+id="container">\s*<div[^>]+id="message_headers">\s*(<dl>.+</dl>)}m)
|
|
86
|
+
return remove_attachments_link(match_data[1]).html_safe if match_data && match_data[1].present?
|
|
87
|
+
|
|
88
|
+
'UNABLE TO PARSE HEADERS'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def plain_text
|
|
92
|
+
@plain_text ||= adjust_link_targets(read_file('plain'))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def rich_text
|
|
96
|
+
@rich_text ||= adjust_link_targets(read_file('rich'))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attachments
|
|
100
|
+
@attachments ||= begin
|
|
101
|
+
response = aws_client.list_objects_v2(
|
|
102
|
+
bucket: self.class.bucket,
|
|
103
|
+
prefix: "#{self.class.bucket_prefix}/#{id}/attachments/"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response.contents.each_with_object({}) do |obj, hash|
|
|
107
|
+
filename = File.basename(obj.key)
|
|
108
|
+
# Generate presigned URL valid for 1 week
|
|
109
|
+
hash[filename] = presigned_url(obj.key)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def delete
|
|
115
|
+
return unless valid?
|
|
116
|
+
|
|
117
|
+
response = aws_client.list_objects_v2(
|
|
118
|
+
bucket: self.class.bucket,
|
|
119
|
+
prefix: "#{self.class.bucket_prefix}/#{id}/"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return if response.contents.empty?
|
|
123
|
+
|
|
124
|
+
objects_to_delete = response.contents.map { |obj| { key: obj.key } }
|
|
125
|
+
|
|
126
|
+
aws_client.delete_objects(
|
|
127
|
+
bucket: self.class.bucket,
|
|
128
|
+
delete: { objects: objects_to_delete }
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def valid?
|
|
133
|
+
file_exists?('rich') || file_exists?('plain')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_param
|
|
137
|
+
id
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def default_style
|
|
141
|
+
file_exists?('rich') ? 'rich' : 'plain'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def send_attachment(controller, filename)
|
|
145
|
+
return unless attachments.key?(filename)
|
|
146
|
+
|
|
147
|
+
response = aws_client.get_object(
|
|
148
|
+
bucket: self.class.bucket,
|
|
149
|
+
key: "#{self.class.bucket_prefix}/#{id}/attachments/#{filename}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
content = response.body.read
|
|
153
|
+
content_type = response.content_type || 'application/octet-stream'
|
|
154
|
+
|
|
155
|
+
controller.send_data(content, filename: filename, type: content_type, disposition: 'inline')
|
|
156
|
+
rescue Aws::S3::Errors::NoSuchKey
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def aws_client
|
|
163
|
+
self.class.aws_client
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def read_file(style)
|
|
167
|
+
response = aws_client.get_object(
|
|
168
|
+
bucket: self.class.bucket,
|
|
169
|
+
key: "#{self.class.bucket_prefix}/#{id}/#{style}.html"
|
|
170
|
+
)
|
|
171
|
+
response.body.read
|
|
172
|
+
rescue Aws::S3::Errors::NoSuchKey
|
|
173
|
+
''
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def file_exists?(style)
|
|
177
|
+
aws_client.head_object(
|
|
178
|
+
bucket: self.class.bucket,
|
|
179
|
+
key: "#{self.class.bucket_prefix}/#{id}/#{style}.html"
|
|
180
|
+
)
|
|
181
|
+
true
|
|
182
|
+
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def presigned_url(key)
|
|
187
|
+
signer = Aws::S3::Presigner.new(client: aws_client)
|
|
188
|
+
signer.presigned_url(
|
|
189
|
+
:get_object,
|
|
190
|
+
bucket: self.class.bucket,
|
|
191
|
+
key: key,
|
|
192
|
+
expires_in: 7 * 24 * 60 * 60 # 1 week
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def remove_attachments_link(headers)
|
|
197
|
+
xml = REXML::Document.new(headers)
|
|
198
|
+
label_element = xml.root.elements.find { |e| e.get_text&.value&.match?(/attachments:/i) }
|
|
199
|
+
|
|
200
|
+
if label_element
|
|
201
|
+
xml.root.delete_element(label_element.next_element)
|
|
202
|
+
xml.root.delete_element(label_element)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
xml.to_s
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def adjust_link_targets(contents)
|
|
209
|
+
return contents if contents.blank?
|
|
210
|
+
|
|
211
|
+
contents.scan(%r{<a\s[^>]+>(?:.|\s)*?</a>}).each do |link|
|
|
212
|
+
fixed_link = fix_link_html(link)
|
|
213
|
+
xml = REXML::Document.new(fixed_link).root
|
|
214
|
+
next if xml.attributes['href'] =~ /(plain|rich).html/
|
|
215
|
+
|
|
216
|
+
xml.attributes['target'] = '_blank'
|
|
217
|
+
xml.add_text('') unless xml.text
|
|
218
|
+
contents.gsub!(link, xml.to_s)
|
|
219
|
+
end
|
|
220
|
+
contents
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def fix_link_html(link_html)
|
|
224
|
+
link_html.dup.tap do |fixed_link|
|
|
225
|
+
fixed_link.gsub!('<br>', '<br/>')
|
|
226
|
+
fixed_link.scan(/<img(?:[^>]+?)>/).each do |img|
|
|
227
|
+
fixed_img = img.dup
|
|
228
|
+
fixed_img.gsub!(/>$/, '/>') unless img =~ %r{/>$}
|
|
229
|
+
fixed_link.gsub!(img, fixed_img)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'letter_opener'
|
|
4
|
+
|
|
5
|
+
module LetterOpenerWeb
|
|
6
|
+
# Extends LetterOpener::Message to upload email content to S3
|
|
7
|
+
#
|
|
8
|
+
# This class intercepts the message delivery and uploads the generated
|
|
9
|
+
# HTML files and attachments to S3 instead of keeping them on local disk.
|
|
10
|
+
class S3Message < LetterOpener::Message
|
|
11
|
+
def render
|
|
12
|
+
# Call parent to generate the HTML files locally first
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
# Upload to S3
|
|
16
|
+
upload_to_s3
|
|
17
|
+
|
|
18
|
+
# Clean up local files after uploading
|
|
19
|
+
cleanup_local_files
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :location
|
|
25
|
+
|
|
26
|
+
def upload_to_s3
|
|
27
|
+
upload_html_files
|
|
28
|
+
upload_attachments
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def upload_html_files
|
|
32
|
+
%w[rich plain].each do |style|
|
|
33
|
+
file_path = "#{location}/#{style}.html"
|
|
34
|
+
next unless File.exist?(file_path)
|
|
35
|
+
|
|
36
|
+
s3_key = "#{bucket_prefix}/#{letter_id}/#{style}.html"
|
|
37
|
+
upload_file(file_path, s3_key, 'text/html')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def upload_attachments
|
|
42
|
+
attachments_dir = "#{location}/attachments"
|
|
43
|
+
return unless Dir.exist?(attachments_dir)
|
|
44
|
+
|
|
45
|
+
Dir.glob("#{attachments_dir}/*").each do |file_path|
|
|
46
|
+
filename = File.basename(file_path)
|
|
47
|
+
s3_key = "#{bucket_prefix}/#{letter_id}/attachments/#{filename}"
|
|
48
|
+
content_type = detect_content_type(file_path)
|
|
49
|
+
upload_file(file_path, s3_key, content_type)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def upload_file(file_path, s3_key, content_type)
|
|
54
|
+
File.open(file_path, 'rb') do |file|
|
|
55
|
+
aws_client.put_object(
|
|
56
|
+
bucket: bucket,
|
|
57
|
+
key: s3_key,
|
|
58
|
+
body: file,
|
|
59
|
+
content_type: content_type
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def cleanup_local_files
|
|
65
|
+
FileUtils.rm_rf(location)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def letter_id
|
|
69
|
+
@letter_id ||= File.basename(location)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def aws_client
|
|
73
|
+
@aws_client ||= Aws::S3::Client.new(
|
|
74
|
+
access_key_id: LetterOpenerWeb.config.aws_access_key_id,
|
|
75
|
+
secret_access_key: LetterOpenerWeb.config.aws_secret_access_key,
|
|
76
|
+
region: LetterOpenerWeb.config.aws_region
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def bucket
|
|
81
|
+
LetterOpenerWeb.config.aws_bucket
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def bucket_prefix
|
|
85
|
+
LetterOpenerWeb.config.aws_bucket_prefix
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def detect_content_type(file_path)
|
|
89
|
+
# Try Marcel if available (comes with Rails/ActiveStorage)
|
|
90
|
+
if defined?(Marcel)
|
|
91
|
+
Marcel::MimeType.for(Pathname.new(file_path))
|
|
92
|
+
else
|
|
93
|
+
# Fallback: let S3 detect or use generic binary
|
|
94
|
+
'application/octet-stream'
|
|
95
|
+
end
|
|
96
|
+
rescue StandardError
|
|
97
|
+
'application/octet-stream'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'letter_opener_web'
|
|
4
|
+
require 'letter_opener_web/s3_letter'
|
|
5
|
+
require 'letter_opener_web/s3_message'
|
|
6
|
+
require 'letter_opener_web/s3_delivery_method'
|
|
7
|
+
|
|
8
|
+
# Add S3-specific configuration to LetterOpenerWeb::Config immediately
|
|
9
|
+
LetterOpenerWeb::Config.class_eval do
|
|
10
|
+
attr_accessor :aws_access_key_id,
|
|
11
|
+
:aws_secret_access_key,
|
|
12
|
+
:aws_region,
|
|
13
|
+
:aws_bucket
|
|
14
|
+
attr_writer :aws_bucket_prefix
|
|
15
|
+
|
|
16
|
+
def aws_bucket_prefix
|
|
17
|
+
@aws_bucket_prefix ||= 'letters'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module LetterOpenerWeb
|
|
22
|
+
module S3
|
|
23
|
+
class Railtie < Rails::Railtie
|
|
24
|
+
initializer 'letter_opener_web_s3.configure' do
|
|
25
|
+
# Register custom delivery method that uses S3DeliveryMethod
|
|
26
|
+
ActionMailer::Base.add_delivery_method(
|
|
27
|
+
:letter_opener_web_s3,
|
|
28
|
+
LetterOpenerWeb::S3DeliveryMethod,
|
|
29
|
+
location: Rails.root.join('tmp', 'letter_opener')
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: letter_opener_web-s3
|
|
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: aws-sdk-s3
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.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 S3-based storage for letter_opener_web, enabling multi-instance
|
|
70
|
+
deployments on Heroku, containers, and other platforms.
|
|
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/s3_delivery_method.rb
|
|
80
|
+
- lib/letter_opener_web/s3_letter.rb
|
|
81
|
+
- lib/letter_opener_web/s3_message.rb
|
|
82
|
+
- lib/letter_opener_web_s3.rb
|
|
83
|
+
homepage: https://github.com/WizaCo/letter_opener_web-s3
|
|
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: Amazon S3 storage backend for letter_opener_web
|
|
106
|
+
test_files: []
|