rails-mailer-sandbox 0.2.0 → 0.2.2
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 +4 -4
- data/.yardopts +8 -0
- data/CHANGELOG.md +55 -0
- data/README.md +48 -28
- data/lib/rails_mailer_sandbox/setup.rb +4 -0
- data/lib/rails_mailer_sandbox.rb +100 -20
- metadata +35 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf80512fe3b9cd60f5842611ee2a68be8b1f5da0f7198872f5aaf0e62fc9c46a
|
|
4
|
+
data.tar.gz: 8436ff5fa82cf18b5dcb6ea7a905264fcf4bcb638aa711a2f0808528c0aee986
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 755b5e8a9368bd55e3c3221494a95aedef5604da4378c32ec62baa84a9754318c5a586a7dc4dd28a4b7a6fb6e1e27409613ed8059d3affba7398f4c6b511d234
|
|
7
|
+
data.tar.gz: b2a089a3a3b475457052833aac2ba039cd76a7ff39fd25d2bf4aac0c573ebd82cd3e6d7440ec2ae18e77bcd4848964092c604fbb1794bdf4ad88a582cd0b70d2
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Rails Mailer Sandbox are documented here.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions
|
|
6
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.2] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Rubydoc.info documentation metadata and YARD annotations
|
|
13
|
+
- Documentation status badge in README
|
|
14
|
+
- `.yardopts` configuration file
|
|
15
|
+
|
|
16
|
+
## [0.2.1] - 2026-09-13
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Inline MIME image rendering through embedded `data:` URLs
|
|
21
|
+
- Safe new-tab behavior for links inside HTML previews
|
|
22
|
+
- RFC 5987 UTF-8 attachment download filenames
|
|
23
|
+
- Lightweight `/status.json` endpoint for inbox polling
|
|
24
|
+
- Recipient labels for Bcc-only and undisclosed-recipient messages
|
|
25
|
+
- Root `Gemfile` and GitHub Actions CI for Ruby 3.0 through 3.3
|
|
26
|
+
- Explicit `base64` dependency for Ruby 3.4 compatibility
|
|
27
|
+
- Regression tests for complex HTML-only and multipart emails
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- CSS and script content leaking into generated plain-text previews
|
|
32
|
+
- Search ignoring body content after first 140 characters
|
|
33
|
+
- Inline `cid:` images appearing broken
|
|
34
|
+
- Links without `target="_blank"` doing nothing inside preview
|
|
35
|
+
- Every polling request parsing all stored `.eml` files
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
|
|
39
|
+
- Preview links escape sandbox only into new tabs; scripts, forms, remote resources,
|
|
40
|
+
and top-level navigation remain blocked
|
|
41
|
+
|
|
42
|
+
## [0.2.0] - 2026-09-13
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Initial RubyGems release
|
|
47
|
+
- Action Mailer delivery method
|
|
48
|
+
- Persistent filesystem-backed `.eml` storage
|
|
49
|
+
- Mountable Rails Web UI with search, HTML/text/raw views, responsive preview widths,
|
|
50
|
+
attachments, automatic refresh, and message deletion
|
|
51
|
+
- `rails_mailer_sandbox:setup` Rake task
|
|
52
|
+
|
|
53
|
+
[0.2.2]: https://github.com/azmi2409/rails-mailer-sandbox/compare/v0.2.1...v0.2.2
|
|
54
|
+
[0.2.1]: https://github.com/azmi2409/rails-mailer-sandbox/compare/v0.2.0...v0.2.1
|
|
55
|
+
[0.2.0]: https://github.com/azmi2409/rails-mailer-sandbox/releases/tag/v0.2.0
|
data/README.md
CHANGED
|
@@ -1,47 +1,55 @@
|
|
|
1
|
-
# Rails Mailer Sandbox
|
|
1
|
+
# Rails Mailer Sandbox 📬
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://github.com/azmi2409/rails-mailer-sandbox/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/rails-mailer-sandbox)
|
|
5
|
+
[](https://rubydoc.info/gems/rails-mailer-sandbox)
|
|
6
|
+
[](MIT-LICENSE)
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
Catch Rails emails locally before they escape into the wild. Rails Mailer Sandbox stores
|
|
9
|
+
real `.eml` messages and gives you a Mailtrap-inspired inbox right inside your Rails app.
|
|
10
|
+
No SMTP server, database, account, or monthly bill needed.
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
## ✨ What You Get
|
|
13
|
+
|
|
14
|
+
- Persistent MIME messages across Rails restarts
|
|
15
|
+
- Search by subject, sender, recipient, or full body
|
|
10
16
|
- HTML, plain-text, and raw-source views
|
|
11
17
|
- Desktop, tablet, and mobile preview widths
|
|
12
18
|
- From, To, Cc, Bcc, date, and message-size details
|
|
13
|
-
-
|
|
14
|
-
-
|
|
19
|
+
- Inline `cid:` image previews and attachment downloads
|
|
20
|
+
- UTF-8 attachment filenames
|
|
21
|
+
- Automatic inbox refresh without repeatedly parsing every email
|
|
15
22
|
- Individual and bulk deletion
|
|
16
|
-
- Sandboxed HTML previews with remote resources
|
|
23
|
+
- Sandboxed HTML previews with scripts and remote resources blocked
|
|
17
24
|
- Filesystem locking and atomic writes for Rails and background workers
|
|
18
|
-
- No database or external service
|
|
19
25
|
|
|
20
|
-
##
|
|
26
|
+
## 🚀 Get Started
|
|
21
27
|
|
|
22
28
|
Add gem to development group:
|
|
23
29
|
|
|
24
30
|
```ruby
|
|
25
31
|
group :development do
|
|
26
|
-
gem "rails-mailer-sandbox"
|
|
32
|
+
gem "rails-mailer-sandbox"
|
|
27
33
|
end
|
|
28
34
|
```
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
Set it up:
|
|
31
37
|
|
|
32
38
|
```bash
|
|
33
39
|
bundle install
|
|
34
40
|
bundle exec rake rails_mailer_sandbox:setup
|
|
35
41
|
```
|
|
36
42
|
|
|
37
|
-
Restart Rails
|
|
43
|
+
Restart Rails, send any Action Mailer email, then visit:
|
|
44
|
+
|
|
45
|
+
👉 <http://localhost:3000/rails_mailer_sandbox>
|
|
38
46
|
|
|
39
47
|
Setup creates `config/initializers/rails_mailer_sandbox.rb`, configures Action Mailer,
|
|
40
|
-
and mounts Web UI.
|
|
48
|
+
and mounts Web UI. Run task again safely; it will not overwrite edited initializer.
|
|
41
49
|
|
|
42
|
-
## Manual Setup
|
|
50
|
+
## 🔧 Manual Setup
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
Prefer doing it yourself? Add this to `config/environments/development.rb`:
|
|
45
53
|
|
|
46
54
|
```ruby
|
|
47
55
|
config.action_mailer.delivery_method = :rails_mailer_sandbox
|
|
@@ -49,35 +57,47 @@ config.action_mailer.perform_deliveries = true
|
|
|
49
57
|
config.action_mailer.raise_delivery_errors = true
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
Mount
|
|
60
|
+
Mount inbox in `config/routes.rb`:
|
|
53
61
|
|
|
54
62
|
```ruby
|
|
55
63
|
mount RailsMailerSandbox::Server.new => "/rails_mailer_sandbox" if Rails.env.development?
|
|
56
64
|
```
|
|
57
65
|
|
|
58
|
-
##
|
|
66
|
+
## 💾 Where Emails Live
|
|
59
67
|
|
|
60
68
|
Default path: `Rails.root/tmp/rails_mailer_sandbox`.
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
Want another location? Set it in initializer:
|
|
63
71
|
|
|
64
72
|
```ruby
|
|
65
73
|
RailsMailerSandbox.storage_path = Rails.root.join("storage", "development_mail")
|
|
66
74
|
```
|
|
67
75
|
|
|
68
|
-
Keep custom storage
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
Keep custom storage out of version control. `bin/rails tmp:clear` removes default inbox.
|
|
77
|
+
Filesystem storage targets local development on one host, not distributed deployments or
|
|
78
|
+
giant email archives.
|
|
71
79
|
|
|
72
|
-
## Development
|
|
80
|
+
## 🧪 Development
|
|
73
81
|
|
|
74
82
|
```bash
|
|
75
|
-
|
|
83
|
+
git clone https://github.com/azmi2409/rails-mailer-sandbox.git
|
|
84
|
+
cd rails-mailer-sandbox
|
|
85
|
+
bundle install
|
|
86
|
+
bundle exec ruby -Ilib test/rails_mailer_sandbox_test.rb
|
|
76
87
|
gem build rails-mailer-sandbox.gemspec
|
|
77
88
|
```
|
|
78
89
|
|
|
79
|
-
|
|
90
|
+
CI runs tests and gem build on Ruby 3.0, 3.1, 3.2, and 3.3.
|
|
91
|
+
Found a bug or have a neat focused improvement? Read [CONTRIBUTING.md](CONTRIBUTING.md),
|
|
92
|
+
then open an issue or pull request. 🙌
|
|
93
|
+
|
|
94
|
+
Release notes live in [CHANGELOG.md](CHANGELOG.md). 📝
|
|
95
|
+
|
|
96
|
+
## 🔒 One Important Rule
|
|
97
|
+
|
|
98
|
+
Use this gem in development only. Captured emails may contain reset links, personal data,
|
|
99
|
+
or other secrets. Never expose mounted inbox publicly or enable it in production.
|
|
80
100
|
|
|
81
|
-
## License
|
|
101
|
+
## 📄 License
|
|
82
102
|
|
|
83
|
-
[MIT](MIT-LICENSE)
|
|
103
|
+
[MIT](MIT-LICENSE). Build useful things and test your emails. 🎉
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module RailsMailerSandbox
|
|
6
|
+
# Generator for sandbox initializer.
|
|
6
7
|
module Setup
|
|
8
|
+
# Generates initializer and configures routes.
|
|
9
|
+
# @param root [String, Pathname] Rails application root path
|
|
10
|
+
# @return [void]
|
|
7
11
|
def self.run(root)
|
|
8
12
|
root = File.expand_path(root)
|
|
9
13
|
raise "Not a Rails application: #{root}" unless File.file?(File.join(root, "config/application.rb"))
|
data/lib/rails_mailer_sandbox.rb
CHANGED
|
@@ -8,22 +8,51 @@ require "time"
|
|
|
8
8
|
require "date"
|
|
9
9
|
require "fileutils"
|
|
10
10
|
require "mail"
|
|
11
|
+
require "base64"
|
|
12
|
+
require "uri"
|
|
11
13
|
|
|
14
|
+
# Module for local Action Mailer email preview and storage sandbox.
|
|
12
15
|
module RailsMailerSandbox
|
|
13
|
-
VERSION = "0.2.
|
|
16
|
+
VERSION = "0.2.2"
|
|
14
17
|
|
|
15
18
|
class << self
|
|
19
|
+
# @return [String, nil]
|
|
16
20
|
attr_writer :storage_path
|
|
17
21
|
|
|
22
|
+
# Directory where captured emails are stored.
|
|
23
|
+
# @return [String]
|
|
18
24
|
def storage_path
|
|
19
25
|
@storage_path || File.join(defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root.to_s : Dir.pwd, "tmp", "rails_mailer_sandbox")
|
|
20
26
|
end
|
|
21
27
|
end
|
|
22
28
|
|
|
29
|
+
# Represents a captured email message.
|
|
23
30
|
class Message
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
# @return [String] Unique message identifier
|
|
32
|
+
attr_reader :id
|
|
33
|
+
# @return [Array<String>] Sender email addresses
|
|
34
|
+
attr_reader :from
|
|
35
|
+
# @return [Array<String>] Recipient email addresses
|
|
36
|
+
attr_reader :to
|
|
37
|
+
# @return [Array<String>] CC recipient email addresses
|
|
38
|
+
attr_reader :cc
|
|
39
|
+
# @return [Array<String>] BCC recipient email addresses
|
|
40
|
+
attr_reader :bcc
|
|
41
|
+
# @return [String] Email subject line
|
|
42
|
+
attr_reader :subject
|
|
43
|
+
# @return [Time, DateTime] Message timestamp
|
|
44
|
+
attr_reader :date
|
|
45
|
+
# @return [String, nil] Plain text body
|
|
46
|
+
attr_reader :text_part
|
|
47
|
+
# @return [String, nil] HTML body
|
|
48
|
+
attr_reader :html_part
|
|
49
|
+
# @return [String] Raw MIME message source
|
|
50
|
+
attr_reader :raw_source
|
|
51
|
+
# @return [Array<Hash{Symbol => Object}>] Parsed attachments
|
|
52
|
+
attr_reader :attachments
|
|
53
|
+
|
|
54
|
+
# @param mail [Mail::Message]
|
|
55
|
+
# @param id [String]
|
|
27
56
|
def initialize(mail, id: SecureRandom.uuid)
|
|
28
57
|
@id = id
|
|
29
58
|
@date = mail.date || Time.now
|
|
@@ -38,46 +67,74 @@ module RailsMailerSandbox
|
|
|
38
67
|
@raw_source = "Bcc: #{@bcc.join(', ').gsub(/[\r\n]/, '')}\r\n#{@raw_source}" unless @bcc.empty? || @raw_source.match?(/^Bcc:/i)
|
|
39
68
|
end
|
|
40
69
|
|
|
70
|
+
# @return [Array<String>]
|
|
41
71
|
def recipients
|
|
42
72
|
to + cc + bcc
|
|
43
73
|
end
|
|
44
74
|
|
|
75
|
+
# @return [String]
|
|
45
76
|
def preview
|
|
46
77
|
text_part.to_s.gsub(/\s+/, " ").strip[0, 140]
|
|
47
78
|
end
|
|
48
79
|
|
|
80
|
+
# @return [Integer]
|
|
49
81
|
def size
|
|
50
82
|
raw_source.bytesize
|
|
51
83
|
end
|
|
52
84
|
|
|
85
|
+
# @param query [String]
|
|
86
|
+
# @return [Boolean]
|
|
53
87
|
def matches?(query)
|
|
54
|
-
query.empty? || [subject, *from, *recipients,
|
|
88
|
+
query.empty? || [subject, *from, *recipients, text_part.to_s].any? { |value| value.downcase.include?(query) }
|
|
55
89
|
end
|
|
56
90
|
|
|
57
91
|
private
|
|
58
92
|
|
|
59
93
|
def extract_parts(mail)
|
|
60
94
|
parts = mail.multipart? ? mail.all_parts : [mail]
|
|
95
|
+
inline_parts = {}
|
|
61
96
|
parts.each do |part|
|
|
62
97
|
if part.respond_to?(:attachment?) && part.attachment?
|
|
98
|
+
content_id = part.content_id.to_s.delete("<>")
|
|
63
99
|
@attachments << {
|
|
64
100
|
filename: part.filename.to_s,
|
|
65
101
|
content_type: part.mime_type.to_s,
|
|
66
102
|
body: part.body.decoded
|
|
67
103
|
}
|
|
104
|
+
inline_parts[content_id] = part unless content_id.empty?
|
|
105
|
+
inline_parts[part.filename.to_s] = part unless part.filename.to_s.empty?
|
|
68
106
|
elsif part.mime_type == "text/html" && !@html_part
|
|
69
107
|
@html_part = part.body.decoded
|
|
70
108
|
elsif part.mime_type == "text/plain" && !@text_part
|
|
71
109
|
@text_part = part.body.decoded
|
|
72
110
|
end
|
|
73
111
|
end
|
|
74
|
-
|
|
112
|
+
inline_parts.each do |content_id, part|
|
|
113
|
+
data_uri = "data:#{part.mime_type};base64,#{Base64.strict_encode64(part.body.decoded)}"
|
|
114
|
+
@html_part&.gsub!(/cid:#{Regexp.escape(content_id)}/i, data_uri)
|
|
115
|
+
end
|
|
116
|
+
@html_part = with_external_link_target(@html_part) if @html_part
|
|
117
|
+
@text_part ||= @html_part&.gsub(/<style\b[^>]*>.*?<\/style>/im, " ")
|
|
118
|
+
&.gsub(/<script\b[^>]*>.*?<\/script>/im, " ")
|
|
119
|
+
&.gsub(/<[^>]*>/, " ")&.then { |text| CGI.unescapeHTML(text) }&.gsub(/\s+/, " ")&.strip
|
|
75
120
|
@html_part ||= "<pre style=\"white-space:pre-wrap\">#{CGI.escapeHTML(@text_part.to_s)}</pre>"
|
|
76
121
|
end
|
|
122
|
+
|
|
123
|
+
def with_external_link_target(html)
|
|
124
|
+
if html.match?(/<head\b[^>]*>/i)
|
|
125
|
+
html.sub(/<head\b[^>]*>/i) { |head| "#{head}<base target=\"_blank\">" }
|
|
126
|
+
else
|
|
127
|
+
"<base target=\"_blank\">#{html}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
77
130
|
end
|
|
78
131
|
|
|
132
|
+
# Thread-safe filesystem storage for captured emails.
|
|
79
133
|
class Storage
|
|
80
134
|
class << self
|
|
135
|
+
# Stores an email message.
|
|
136
|
+
# @param mail [Mail::Message]
|
|
137
|
+
# @return [Message]
|
|
81
138
|
def add(mail)
|
|
82
139
|
message = Message.new(mail)
|
|
83
140
|
locked do |directory|
|
|
@@ -96,6 +153,8 @@ module RailsMailerSandbox
|
|
|
96
153
|
message
|
|
97
154
|
end
|
|
98
155
|
|
|
156
|
+
# Returns all stored messages, ordered newest first.
|
|
157
|
+
# @return [Array<Message>]
|
|
99
158
|
def all
|
|
100
159
|
locked do |directory|
|
|
101
160
|
Dir.glob(File.join(directory, "*.eml")).sort_by { |path| [File.mtime(path), path] }.reverse.map do |path|
|
|
@@ -104,6 +163,9 @@ module RailsMailerSandbox
|
|
|
104
163
|
end
|
|
105
164
|
end
|
|
106
165
|
|
|
166
|
+
# Finds message by UUID.
|
|
167
|
+
# @param id [String]
|
|
168
|
+
# @return [Message, nil]
|
|
107
169
|
def find(id)
|
|
108
170
|
return unless valid_id?(id)
|
|
109
171
|
locked do |directory|
|
|
@@ -112,6 +174,9 @@ module RailsMailerSandbox
|
|
|
112
174
|
end
|
|
113
175
|
end
|
|
114
176
|
|
|
177
|
+
# Deletes message by UUID.
|
|
178
|
+
# @param id [String]
|
|
179
|
+
# @return [Boolean, nil]
|
|
115
180
|
def delete(id)
|
|
116
181
|
return unless valid_id?(id)
|
|
117
182
|
locked do |directory|
|
|
@@ -120,10 +185,14 @@ module RailsMailerSandbox
|
|
|
120
185
|
end
|
|
121
186
|
end
|
|
122
187
|
|
|
188
|
+
# Clears all stored messages.
|
|
189
|
+
# @return [Array<String>]
|
|
123
190
|
def clear
|
|
124
191
|
locked { |directory| Dir.glob(File.join(directory, "*.eml")).each { |path| File.delete(path) } }
|
|
125
192
|
end
|
|
126
193
|
|
|
194
|
+
# Returns count of stored messages.
|
|
195
|
+
# @return [Integer]
|
|
127
196
|
def count
|
|
128
197
|
locked { |directory| Dir.glob(File.join(directory, "*.eml")).size }
|
|
129
198
|
end
|
|
@@ -148,18 +217,25 @@ module RailsMailerSandbox
|
|
|
148
217
|
end
|
|
149
218
|
end
|
|
150
219
|
|
|
220
|
+
# Action Mailer delivery method for sandbox storage.
|
|
151
221
|
class DeliveryMethod
|
|
222
|
+
# @return [Hash]
|
|
152
223
|
attr_accessor :settings
|
|
153
224
|
|
|
225
|
+
# @param values [Hash]
|
|
154
226
|
def initialize(values = {})
|
|
155
227
|
@settings = values
|
|
156
228
|
end
|
|
157
229
|
|
|
230
|
+
# Stores incoming email.
|
|
231
|
+
# @param mail [Mail::Message]
|
|
232
|
+
# @return [Message]
|
|
158
233
|
def deliver!(mail)
|
|
159
234
|
Storage.add(mail)
|
|
160
235
|
end
|
|
161
236
|
end
|
|
162
237
|
|
|
238
|
+
# Rack application providing Web UI to inspect captured emails.
|
|
163
239
|
class Server
|
|
164
240
|
SECURITY_HEADERS = {
|
|
165
241
|
"content-security-policy" => "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; frame-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'self'",
|
|
@@ -168,6 +244,9 @@ module RailsMailerSandbox
|
|
|
168
244
|
"x-frame-options" => "SAMEORIGIN"
|
|
169
245
|
}.freeze
|
|
170
246
|
|
|
247
|
+
# Rack call entrypoint.
|
|
248
|
+
# @param env [Hash]
|
|
249
|
+
# @return [Array]
|
|
171
250
|
def call(env)
|
|
172
251
|
request = Rack::Request.new(env)
|
|
173
252
|
response = route(request)
|
|
@@ -189,7 +268,7 @@ module RailsMailerSandbox
|
|
|
189
268
|
end
|
|
190
269
|
|
|
191
270
|
return render_index(request) if method == "GET" && ["", "/"].include?(path)
|
|
192
|
-
return
|
|
271
|
+
return render_status_json if method == "GET" && path == "/status.json"
|
|
193
272
|
return render_part(Regexp.last_match(1), :html_part, "text/html; charset=utf-8") if method == "GET" && path =~ %r{\A/messages/([^/]+)/html\z}
|
|
194
273
|
return render_part(Regexp.last_match(1), :raw_source, "text/plain; charset=utf-8") if method == "GET" && path =~ %r{\A/messages/([^/]+)/raw\z}
|
|
195
274
|
return render_attachment(Regexp.last_match(1), Regexp.last_match(2).to_i) if method == "GET" && path =~ %r{\A/messages/([^/]+)/attachment/(\d+)\z}
|
|
@@ -209,7 +288,7 @@ module RailsMailerSandbox
|
|
|
209
288
|
return not_found unless message
|
|
210
289
|
|
|
211
290
|
headers = { "content-type" => content_type }
|
|
212
|
-
headers["content-security-policy"] = "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:; base-uri '
|
|
291
|
+
headers["content-security-policy"] = "sandbox allow-popups allow-popups-to-escape-sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:; base-uri 'self'; form-action 'none'" if field == :html_part
|
|
213
292
|
[200, headers, [message.public_send(field).to_s]]
|
|
214
293
|
end
|
|
215
294
|
|
|
@@ -217,20 +296,19 @@ module RailsMailerSandbox
|
|
|
217
296
|
attachment = Storage.find(id)&.attachments&.[](index)
|
|
218
297
|
return not_found unless attachment
|
|
219
298
|
|
|
220
|
-
|
|
299
|
+
original_filename = attachment[:filename].encode("UTF-8", invalid: :replace, undef: :replace).gsub(/[\x00-\x1f\x7f]/, "_")
|
|
300
|
+
filename = original_filename.encode("US-ASCII", invalid: :replace, undef: :replace, replace: "_").gsub(/["\\]/, "_")
|
|
301
|
+
encoded_filename = URI.encode_www_form_component(original_filename).tr("+", "%20")
|
|
221
302
|
type = attachment[:content_type].match?(/\A[-\w.+]+\/[-\w.+]+\z/) ? attachment[:content_type] : "application/octet-stream"
|
|
222
303
|
[200, {
|
|
223
304
|
"content-type" => type,
|
|
224
|
-
"content-disposition" => %(attachment; filename="#{filename}"),
|
|
305
|
+
"content-disposition" => %(attachment; filename="#{filename}"; filename*=UTF-8''#{encoded_filename}),
|
|
225
306
|
"content-length" => attachment[:body].bytesize.to_s
|
|
226
307
|
}, [attachment[:body]]]
|
|
227
308
|
end
|
|
228
309
|
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
{ id: message.id, subject: message.subject, from: message.from, to: message.to, date: message.date.iso8601 }
|
|
232
|
-
end
|
|
233
|
-
[200, { "content-type" => "application/json; charset=utf-8", "cache-control" => "no-store" }, [JSON.generate(payload)]]
|
|
310
|
+
def render_status_json
|
|
311
|
+
[200, { "content-type" => "application/json; charset=utf-8", "cache-control" => "no-store" }, [JSON.generate(count: Storage.count)]]
|
|
234
312
|
end
|
|
235
313
|
|
|
236
314
|
def render_index(request)
|
|
@@ -286,6 +364,8 @@ module RailsMailerSandbox
|
|
|
286
364
|
messages.map do |message|
|
|
287
365
|
params = "?id=#{Rack::Utils.escape(message.id)}"
|
|
288
366
|
params += "&q=#{Rack::Utils.escape(query)}" unless query.empty?
|
|
367
|
+
recipient = message.to.first || message.recipients.first
|
|
368
|
+
recipient_label = recipient ? "to #{recipient}" : "undisclosed recipients"
|
|
289
369
|
<<~HTML
|
|
290
370
|
<a class="message #{"active" if message == selected}" href="#{base}#{params}">
|
|
291
371
|
<span class="avatar">#{h(initials(message.from.first))}</span>
|
|
@@ -293,7 +373,7 @@ module RailsMailerSandbox
|
|
|
293
373
|
<span class="message-line"><strong>#{h(message.from.first || "Unknown sender")}</strong><time>#{h(short_time(message.date))}</time></span>
|
|
294
374
|
<span class="subject">#{h(message.subject.empty? ? "(no subject)" : message.subject)}</span>
|
|
295
375
|
<span class="preview">#{h(message.preview)}</span>
|
|
296
|
-
<span class="recipient"
|
|
376
|
+
<span class="recipient">#{h(recipient_label)}</span>
|
|
297
377
|
</span>
|
|
298
378
|
</a>
|
|
299
379
|
HTML
|
|
@@ -332,7 +412,7 @@ module RailsMailerSandbox
|
|
|
332
412
|
<button class="viewport" data-width="390px" title="Mobile preview">▯</button>
|
|
333
413
|
</nav>
|
|
334
414
|
<section class="preview-area">
|
|
335
|
-
<div class="pane active" id="pane-html"><iframe title="Email HTML preview" sandbox="allow-popups" src="#{base}/messages/#{message.id}/html"></iframe></div>
|
|
415
|
+
<div class="pane active" id="pane-html"><iframe title="Email HTML preview" sandbox="allow-popups allow-popups-to-escape-sandbox" src="#{base}/messages/#{message.id}/html"></iframe></div>
|
|
336
416
|
<pre class="pane source" id="pane-text">#{h(message.text_part.to_s)}</pre>
|
|
337
417
|
<pre class="pane source" id="pane-raw">#{h(message.raw_source.to_s)}</pre>
|
|
338
418
|
</section>
|
|
@@ -405,9 +485,9 @@ module RailsMailerSandbox
|
|
|
405
485
|
setInterval(async () => {
|
|
406
486
|
if (document.querySelector('input[name=q]').value) return;
|
|
407
487
|
try {
|
|
408
|
-
const response = await fetch('#{base}/
|
|
409
|
-
const
|
|
410
|
-
if (
|
|
488
|
+
const response = await fetch('#{base}/status.json', { headers: { accept: 'application/json' } });
|
|
489
|
+
const status = await response.json();
|
|
490
|
+
if (status.count !== Number(document.body.dataset.count)) location.reload();
|
|
411
491
|
} catch (_) {}
|
|
412
492
|
}, 3000);
|
|
413
493
|
JS
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-mailer-sandbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- azmimuwahid
|
|
@@ -37,6 +37,34 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.8'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: base64
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: test-unit
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
40
68
|
description: Intercepts ActionMailer emails in development and provides mountable
|
|
41
69
|
Web UI to preview them.
|
|
42
70
|
email:
|
|
@@ -45,6 +73,8 @@ executables: []
|
|
|
45
73
|
extensions: []
|
|
46
74
|
extra_rdoc_files: []
|
|
47
75
|
files:
|
|
76
|
+
- ".yardopts"
|
|
77
|
+
- CHANGELOG.md
|
|
48
78
|
- CONTRIBUTING.md
|
|
49
79
|
- MIT-LICENSE
|
|
50
80
|
- README.md
|
|
@@ -55,7 +85,10 @@ files:
|
|
|
55
85
|
homepage: https://github.com/azmi2409/rails-mailer-sandbox
|
|
56
86
|
licenses:
|
|
57
87
|
- MIT
|
|
58
|
-
metadata:
|
|
88
|
+
metadata:
|
|
89
|
+
documentation_uri: https://rubydoc.info/gems/rails-mailer-sandbox
|
|
90
|
+
source_code_uri: https://github.com/azmi2409/rails-mailer-sandbox
|
|
91
|
+
changelog_uri: https://github.com/azmi2409/rails-mailer-sandbox/blob/main/CHANGELOG.md
|
|
59
92
|
rdoc_options: []
|
|
60
93
|
require_paths:
|
|
61
94
|
- lib
|