mailscope 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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE.txt +27 -0
  4. data/README.md +210 -0
  5. data/app/assets/mailscope/INTER-LICENSE.txt +92 -0
  6. data/app/assets/mailscope/favicon.svg +6 -0
  7. data/app/assets/mailscope/inter-latin-ext.woff2 +0 -0
  8. data/app/assets/mailscope/inter-latin.woff2 +0 -0
  9. data/app/assets/mailscope/mailscope.css +594 -0
  10. data/app/assets/mailscope/mailscope.js +559 -0
  11. data/app/controllers/mailscope/application_controller.rb +30 -0
  12. data/app/controllers/mailscope/assets_controller.rb +24 -0
  13. data/app/controllers/mailscope/messages_controller.rb +139 -0
  14. data/app/helpers/mailscope/application_helper.rb +124 -0
  15. data/app/views/layouts/mailscope/application.html.erb +30 -0
  16. data/app/views/mailscope/messages/_blank_pane.html.erb +9 -0
  17. data/app/views/mailscope/messages/_list.html.erb +34 -0
  18. data/app/views/mailscope/messages/_list_item.html.erb +39 -0
  19. data/app/views/mailscope/messages/_pane.html.erb +151 -0
  20. data/app/views/mailscope/messages/_rail.html.erb +33 -0
  21. data/app/views/mailscope/messages/_shortcuts.html.erb +21 -0
  22. data/app/views/mailscope/messages/index.html.erb +66 -0
  23. data/app/views/mailscope/shared/_icons.html.erb +58 -0
  24. data/config/locales/mailscope.en.yml +105 -0
  25. data/config/locales/mailscope.pt-BR.yml +105 -0
  26. data/config/routes.rb +24 -0
  27. data/lib/mailscope/body_renderer.rb +142 -0
  28. data/lib/mailscope/configuration.rb +65 -0
  29. data/lib/mailscope/delivery_method.rb +45 -0
  30. data/lib/mailscope/engine.rb +21 -0
  31. data/lib/mailscope/mailbox.rb +39 -0
  32. data/lib/mailscope/message.rb +268 -0
  33. data/lib/mailscope/query.rb +54 -0
  34. data/lib/mailscope/storage/base.rb +35 -0
  35. data/lib/mailscope/storage/filesystem.rb +214 -0
  36. data/lib/mailscope/storage.rb +23 -0
  37. data/lib/mailscope/version.rb +5 -0
  38. data/lib/mailscope.rb +60 -0
  39. metadata +128 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db87fb72ccde481718a35a6a8902eeb1e2fbfc423c8292e17b0d5fb3b97afa0e
4
+ data.tar.gz: 85b4a41a9c9afb1231092a1030e631ca22fef3b9113a24b8b734281343dd6de7
5
+ SHA512:
6
+ metadata.gz: df401455be0fd9677dbd74b2c2aed038277370f61b8f15ec8f0e841bb31c39365fafdaa20ea691d1faa9660d0eb93fb0b29c53f010037e2f59b8fabe29916430
7
+ data.tar.gz: ea923fff2de475991245b5850b26ef21a9619588f3b4b922e9989d95d75d546c7319ba220d0c7fb861736f79a0231ca5833a29b8953cfdb2a9c3e6d2b39f1e6b
data/CHANGELOG.md ADDED
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ [Unreleased]: https://github.com/renatomeloferraz/mailscope/compare/v0.1.0...HEAD
6
+
7
+ ## [0.1.0] — 2026-08-22
8
+
9
+ [0.1.0]: https://github.com/renatomeloferraz/mailscope/releases/tag/v0.1.0
10
+
11
+ First release. A fork of `letter_opener_web` 3.0.0, rebuilt.
12
+
13
+ ### New foundation
14
+
15
+ - Own delivery method, storing the original `.eml` plus a `metadata.json` per
16
+ message. Headers come from the `mail` gem instead of a regex over rendered
17
+ HTML — no more `UNABLE TO PARSE HEADERS`, and no more `letter_opener`
18
+ dependency.
19
+ - Storage adapters (`Mailscope::Storage::Base`), with `:filesystem` built in.
20
+ - Lexicographically sortable, time-ordered ids: listing no longer stats every
21
+ directory nor reads every message in full.
22
+ - Retention policy (`max_letters`, `max_age`) applied after each delivery.
23
+
24
+ ### Requirements
25
+
26
+ - Ruby >= 3.1, Rails >= 7.1. CI runs the suite against Rails 7.1, 7.2, 8.0 and
27
+ 8.1 on the Ruby versions each of them supports.
28
+
29
+ ### Interface
30
+
31
+ - Three-column layout: mailboxes per recipient, message list, reading pane.
32
+ - Search across subject, sender, recipient, mailer and plain-text body. The
33
+ `mailbox` and `only` query parameters narrow the list further, including on
34
+ the JSON endpoint.
35
+ - HTML, Text, Source (raw `.eml`), Headers and Attachments tabs.
36
+ - Responsive preview (desktop / tablet 768px / mobile 390px).
37
+ - Opt-in live updates: the list refreshes itself when new mail arrives, and
38
+ the switch state is remembered per browser.
39
+ - Real light/dark theme with a persisted one-click toggle, starting from the
40
+ OS preference. The HTML preview stays on white on purpose; the plain-text
41
+ view follows the theme.
42
+ - Inter as the interface typeface, bundled with the gem.
43
+ - Keyboard navigation and a shortcuts dialog.
44
+ - `.eml` and attachment downloads.
45
+ - The document title carries the captured message count and tracks the list.
46
+ - Internationalisation: English and Brazilian Portuguese.
47
+
48
+ ### Security
49
+
50
+ - First-class `config.authenticate_with`.
51
+ - The preview runs in a sandboxed iframe under a strict CSP; scripts never run
52
+ and remote content is blocked by default, with a visible count.
53
+ - Inline (`cid:`) images become `data:` URIs, so the frame needs no
54
+ `allow-same-origin`.
55
+
56
+ ### Front end
57
+
58
+ - jQuery, jquery-ujs, Bootstrap and Bootstrap Icons removed: roughly 277 KB
59
+ inlined into every HTML response and re-run through ERB on each request.
60
+ Replaced by own CSS and JS served as immutably cached assets.
61
+ - No `data-remote` / `data-method`: destructive actions use `fetch` with
62
+ `DELETE` and a CSRF token, which also removes the Turbo conflict.
63
+
64
+ ### Compatibility
65
+
66
+ - `:letter_opener_web` is still registered as a delivery method.
67
+ - `config.letters_location` aliases `config.location`.
68
+ - Old `letter_opener` directories are listed read-only.
data/LICENSE.txt ADDED
@@ -0,0 +1,27 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Renato Ferraz
4
+ Copyright (c) 2013-2026 Fabio Rehm, David Muto and letter_opener_web contributors
5
+
6
+ Mailscope started as a fork of letter_opener_web
7
+ (https://github.com/fgrehm/letter_opener_web), which is itself inspired by
8
+ letter_opener (https://github.com/ryanb/letter_opener). Both are MIT licensed
9
+ and their copyright notices are preserved above.
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # Mailscope
2
+
3
+ A mail inspector for Rails development and staging environments. It captures
4
+ everything your app sends and gives you a fast web UI to **search, preview and
5
+ inspect** it: HTML, plain text, the raw `.eml`, real headers and attachments —
6
+ with one mailbox per recipient, responsive previews and remote-content
7
+ blocking.
8
+
9
+ Mailscope started as a fork of
10
+ [letter_opener_web](https://github.com/fgrehm/letter_opener_web) and was
11
+ rebuilt on a different foundation: instead of regex-matching the HTML that
12
+ `letter_opener` already rendered, it stores the original `.eml` plus a small
13
+ metadata index, and reads everything from there.
14
+
15
+ ## Installation
16
+
17
+ Requires Ruby 3.1+ and Rails 7.1+.
18
+
19
+ ```ruby
20
+ group :development do
21
+ gem 'mailscope'
22
+ end
23
+ ```
24
+
25
+ `config/routes.rb`:
26
+
27
+ ```ruby
28
+ Rails.application.routes.draw do
29
+ mount Mailscope::Engine, at: '/mailscope' if Rails.env.development?
30
+ end
31
+ ```
32
+
33
+ `config/environments/development.rb`:
34
+
35
+ ```ruby
36
+ config.action_mailer.delivery_method = :mailscope
37
+ ```
38
+
39
+ That's it. Send an email and open `http://localhost:3000/mailscope`.
40
+
41
+ ## What it does
42
+
43
+ | | |
44
+ |---|---|
45
+ | **Mailboxes per recipient** | One entry per address that received mail (To, Cc and Bcc), with counts. |
46
+ | **Search** | Subject, sender, recipient, mailer class and the plain-text body. |
47
+ | **HTML / Text / Source / Headers / Attachments tabs** | The raw `.eml` and the real headers, not an approximation. |
48
+ | **Responsive preview** | Switch the preview width between desktop, tablet (768px) and mobile (390px). |
49
+ | **Remote-content blocking** | The preview runs in a sandboxed iframe under a strict CSP; external images are blocked by default and the app tells you how many. Inline (`cid:`) images become `data:` URIs. |
50
+ | **Live updates** | Opt-in: flip the switch in the header and the list refreshes itself when new mail arrives. The choice sticks per browser. |
51
+ | **Light / dark theme** | A real theme with a one-click toggle — not a `filter: invert()` hack. The HTML preview deliberately stays on white: it is the message as its recipients will see it. The plain-text view, which has no sender design to preserve, follows the theme. |
52
+ | **Keyboard** | `j`/`k` to move, `/` to search, `x` to delete, `1`–`5` to switch tabs, `?` for the full list. |
53
+ | **`.eml` download** | Reopen a message in any client, or attach it to a bug report. |
54
+ | **JSON API** | `GET /mailscope/?format=json` — handy in system tests. Accepts `q`, `mailbox` and `only=attachments\|html\|text`. |
55
+ | **Retention** | Old messages are discarded automatically; `tmp/` does not grow forever. |
56
+ | **i18n** | English and Brazilian Portuguese, following the app's `I18n.locale`. |
57
+
58
+ ## Configuration
59
+
60
+ ```ruby
61
+ Mailscope.configure do |config|
62
+ # Where messages live. Default: Rails.root.join('tmp', 'mailscope')
63
+ config.location = Rails.root.join('tmp', 'mailscope')
64
+
65
+ # :filesystem (default) or any object implementing Mailscope::Storage::Base
66
+ config.storage = :filesystem
67
+
68
+ # Retention, applied after every delivery. nil disables the check.
69
+ config.max_letters = 500
70
+ config.max_age = 7.days
71
+
72
+ # Access guard. Receives the ActionDispatch::Request; return falsy to deny.
73
+ config.authenticate_with = ->(request) { request.local? }
74
+
75
+ # Initial state of the "remote content" switch in the preview pane.
76
+ config.block_remote_content = true
77
+
78
+ # Browser polling for newly delivered mail. This is only the default state of
79
+ # the header switch — the user's own choice is remembered per browser.
80
+ config.auto_refresh = false
81
+ config.auto_refresh_interval = 3 # seconds
82
+
83
+ # :system (default), :light or :dark — the user can override it in the UI.
84
+ config.default_theme = :system
85
+
86
+ config.title = 'Mailscope'
87
+ end
88
+ ```
89
+
90
+ ### Authentication
91
+
92
+ People routinely mount tools like this on staging. Rather than relying on you
93
+ remembering to wrap the route in a constraint, `authenticate_with` is part of
94
+ the configuration:
95
+
96
+ ```ruby
97
+ Mailscope.configure do |config|
98
+ config.authenticate_with = lambda do |request|
99
+ ActiveSupport::SecurityUtils.secure_compare(
100
+ request.headers['X-Mailscope-Token'].to_s, ENV.fetch('MAILSCOPE_TOKEN')
101
+ )
102
+ end
103
+ end
104
+ ```
105
+
106
+ The block is evaluated in the controller's context, so `session`, `cookies` and
107
+ your application's helpers are available.
108
+
109
+ ## Staging / pre-production use
110
+
111
+ 1. Move the gem out of the `:development` group in your `Gemfile`.
112
+ 2. Set `config.action_mailer.delivery_method = :mailscope` for that environment.
113
+ 3. Mount the route there and **configure `authenticate_with`**.
114
+
115
+ The `:filesystem` adapter keeps messages on the process's own disk. With more
116
+ than one instance (several dynos, a separate worker) each instance only sees
117
+ what it delivered itself — write a shared storage adapter
118
+ (`Mailscope::Storage::Base` has seven methods) and point `config.storage` at it.
119
+
120
+ ## Preview security
121
+
122
+ The message body is served from its own route and rendered inside an iframe
123
+ with `sandbox` (no `allow-scripts`, no `allow-same-origin`) under a strict
124
+ response `Content-Security-Policy`:
125
+
126
+ ```
127
+ default-src 'none'; script-src 'none'; frame-src 'none'; object-src 'none';
128
+ form-action 'none'; base-uri 'none'; style-src 'unsafe-inline' data:;
129
+ font-src data:; img-src data:
130
+ ```
131
+
132
+ With the remote-content switch on, `img-src` additionally accepts `https:` and
133
+ `http:`. Scripts never run.
134
+
135
+ ## Migrating from letter_opener_web
136
+
137
+ ```diff
138
+ -gem 'letter_opener_web', '~> 3.0'
139
+ +gem 'mailscope'
140
+ ```
141
+
142
+ ```diff
143
+ -mount LetterOpenerWeb::Engine, at: '/letter_opener'
144
+ +mount Mailscope::Engine, at: '/mailscope'
145
+ ```
146
+
147
+ The `:letter_opener_web` delivery method stays registered as an alias, so
148
+ `config.action_mailer.delivery_method` does not have to change right away.
149
+ `config.letters_location` also still works as an alias of `config.location`,
150
+ and old `letter_opener` directories (`1358825621_ba83a22/rich.html`) are listed
151
+ read-only.
152
+
153
+ Worth knowing:
154
+
155
+ - New messages are stored as `.eml` + `metadata.json`, not as rendered HTML.
156
+ `letter_opener` is no longer a dependency.
157
+ - Routes changed (`/messages/:id`, `DELETE` instead of `POST .../delete`).
158
+ - No jQuery, jquery-ujs or Bootstrap inlined into the HTML any more.
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ bin/setup
164
+ bin/rspec
165
+ bin/rubocop
166
+
167
+ # against another Rails version
168
+ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle install
169
+ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rspec
170
+ ```
171
+
172
+ To run the bundled example app with sample messages:
173
+
174
+ ```bash
175
+ bundle exec rackup spec/dummy/config.ru -p 3123
176
+ ```
177
+
178
+ Then open `http://localhost:3123/seed` to generate messages and
179
+ `http://localhost:3123/mailscope` for the UI. `MAILSCOPE_LOCALE=en` runs it in
180
+ English, `pt-BR` in Portuguese.
181
+
182
+ ## Bundled assets
183
+
184
+ The UI ships [Inter](https://rsms.me/inter/) (SIL OFL 1.1, see
185
+ [INTER-LICENSE.txt](app/assets/mailscope/INTER-LICENSE.txt)) as two woff2
186
+ subsets served from the engine's own digested asset path. Nothing is fetched
187
+ from a CDN, so the UI looks the same offline and no request leaves the machine.
188
+
189
+ ## Releasing
190
+
191
+ Publishing runs from a tag, authenticated with RubyGems trusted publishing
192
+ (OIDC) — there is no API key in the repository secrets.
193
+
194
+ 1. Bump `Mailscope::VERSION` and move the `Unreleased` notes into a dated
195
+ section in [CHANGELOG.md](CHANGELOG.md).
196
+ 2. Commit, then `git tag vX.Y.Z && git push --tags`.
197
+
198
+ The release workflow reruns the suite, checks the tag against the version,
199
+ then builds and pushes the gem via `rake release`. Write the GitHub release
200
+ notes by hand afterwards if you want them.
201
+
202
+ ## Credits
203
+
204
+ Mailscope is a fork of [letter_opener_web](https://github.com/fgrehm/letter_opener_web)
205
+ by Fabio Rehm and David Muto, which in turn grew out of
206
+ [letter_opener](https://github.com/ryanb/letter_opener) by Ryan Bates. Both MIT.
207
+
208
+ ## License
209
+
210
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,92 @@
1
+ Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION AND CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="8" fill="#5a5bd6"/>
3
+ <path d="M7 11.5A1.5 1.5 0 0 1 8.5 10h15a1.5 1.5 0 0 1 1.5 1.5v9a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 7 20.5z"
4
+ fill="none" stroke="#fff" stroke-width="2"/>
5
+ <path d="m7.5 11.5 8.5 6 8.5-6" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
6
+ </svg>