mail_dude 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 +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +237 -0
- data/app/assets/images/mail_dude/icon.png +0 -0
- data/app/assets/stylesheets/mail_dude/application.css +180 -0
- data/app/channels/mail_dude/messages_channel.rb +18 -0
- data/app/controllers/mail_dude/application_controller.rb +25 -0
- data/app/controllers/mail_dude/attachments_controller.rb +26 -0
- data/app/controllers/mail_dude/messages_controller.rb +72 -0
- data/app/helpers/mail_dude/application_helper.rb +13 -0
- data/app/models/mail_dude/application_record.rb +7 -0
- data/app/models/mail_dude/stored_email.rb +7 -0
- data/app/views/layouts/mail_dude/application.html.erb +134 -0
- data/app/views/mail_dude/messages/_empty.html.erb +4 -0
- data/app/views/mail_dude/messages/_error.html.erb +5 -0
- data/app/views/mail_dude/messages/_list.html.erb +44 -0
- data/app/views/mail_dude/messages/_message_pane.html.erb +30 -0
- data/app/views/mail_dude/messages/_metadata.html.erb +33 -0
- data/app/views/mail_dude/messages/_tabs.html.erb +14 -0
- data/app/views/mail_dude/messages/error.html.erb +4 -0
- data/app/views/mail_dude/messages/index.html.erb +17 -0
- data/app/views/mail_dude/messages/show.html.erb +2 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20260506170200_create_mail_dude_stored_emails.rb +36 -0
- data/lib/generators/mail_dude/install_generator.rb +49 -0
- data/lib/generators/mail_dude/templates/create_mail_dude_stored_emails.tt +37 -0
- data/lib/generators/mail_dude/templates/initializer.tt +17 -0
- data/lib/mail_dude/attachment_locator.rb +111 -0
- data/lib/mail_dude/configuration.rb +102 -0
- data/lib/mail_dude/dashboard.rb +6 -0
- data/lib/mail_dude/delivery_method.rb +46 -0
- data/lib/mail_dude/engine.rb +24 -0
- data/lib/mail_dude/errors.rb +11 -0
- data/lib/mail_dude/html_body_renderer.rb +49 -0
- data/lib/mail_dude/mailer_metadata_headers.rb +22 -0
- data/lib/mail_dude/message_broadcast.rb +50 -0
- data/lib/mail_dude/message_presenter.rb +136 -0
- data/lib/mail_dude/message_record.rb +17 -0
- data/lib/mail_dude/message_serializer.rb +117 -0
- data/lib/mail_dude/pagination.rb +35 -0
- data/lib/mail_dude/stores/base.rb +106 -0
- data/lib/mail_dude/stores/database_store.rb +93 -0
- data/lib/mail_dude/stores/file_store.rb +126 -0
- data/lib/mail_dude/stores/memory_store.rb +53 -0
- data/lib/mail_dude/version.rb +5 -0
- data/lib/mail_dude.rb +80 -0
- data/lib/tasks/mail_dude.rake +25 -0
- metadata +224 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c4295c748cd2ec159f3c65a9934183b3e306d3cd9279b4c296e04e1735e561d9
|
|
4
|
+
data.tar.gz: fc04779985ad537dc15c46f574885accba01bf9936fdc49b017e477c38b330c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4644564f8c0f6a88f709c93715d6f0c74ed8eec7160f19b829896aab43aa3bc24aaad5f9b51dcd103da9ae26ed8138ecdd082a8090976e6495dfd0b214a7e6e1
|
|
7
|
+
data.tar.gz: 93fdd84bbaf8c540e4d270303490170fc074edbba00953486d95dcdaf4e2bc816e0dae13c0cf2b3071f1cf4c4d8f833f71297c96f621008368c819f43d2fcde3
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MailDude 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.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# MailDude
|
|
2
|
+
|
|
3
|
+
MailDude is a mountable Rails engine and Ruby gem that captures Action Mailer deliveries in development, QA, and test-like environments. It registers an Action Mailer delivery method named `:mail_dude`, stores outgoing email instead of sending it externally, and exposes a mailbox UI for reviewing messages, headers, raw source, and attachments.
|
|
4
|
+
|
|
5
|
+
## Why It Exists
|
|
6
|
+
|
|
7
|
+
Local and QA applications often need realistic email delivery flows without risking real SMTP delivery to customers. MailDude captures the final `Mail::Message` through a delivery method, which prevents SMTP, sendmail, or other external delivery agents from being used.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
group :development, :qa do
|
|
13
|
+
gem "mail_dude"
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run the installer:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bin/rails generate mail_dude:install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For database storage:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bin/rails generate mail_dude:install --database
|
|
27
|
+
bin/rails db:migrate
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Action Mailer Configuration
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
config.action_mailer.delivery_method = :mail_dude
|
|
34
|
+
config.action_mailer.perform_deliveries = true
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
MailDude registers this delivery method when Action Mailer loads:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
41
|
+
add_delivery_method :mail_dude, MailDude::DeliveryMethod
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Secure Mounting
|
|
46
|
+
|
|
47
|
+
MailDude does not authenticate or authorize users. Mount it behind your host application’s existing controls.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
authenticate :user, lambda { |u| Ability.new(u).can?(:manage, MailDude::Dashboard) } do
|
|
51
|
+
mount MailDude::Engine, at: "/mail_dude"
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example CanCanCan-style subject:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class Ability
|
|
59
|
+
include CanCan::Ability
|
|
60
|
+
|
|
61
|
+
def initialize(user)
|
|
62
|
+
return unless user
|
|
63
|
+
|
|
64
|
+
can :manage, MailDude::Dashboard if user.admin?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
MailDude does not depend on Devise, CanCanCan, Sidekiq, Redis, or a host app user model.
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
MailDude.configure do |config|
|
|
75
|
+
config.enabled_environments = %w[development qa test]
|
|
76
|
+
config.storage = :file
|
|
77
|
+
config.storage_path = Rails.root.join("tmp", "mail_dude")
|
|
78
|
+
config.max_messages = 1_000
|
|
79
|
+
config.retention_period = 7.days
|
|
80
|
+
config.max_message_size = 25.megabytes
|
|
81
|
+
config.allow_production = false
|
|
82
|
+
config.capture_attachments = true
|
|
83
|
+
config.capture_mailer_metadata_headers = true
|
|
84
|
+
config.default_per_page = 50
|
|
85
|
+
config.live_updates = false
|
|
86
|
+
config.live_update_stream_name = "mail_dude:#{Rails.env}:messages"
|
|
87
|
+
config.live_update_authorizer = ->(_connection) { false }
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`MailDude.enabled?`, `MailDude.store`, `MailDude.reset_store!`, and `MailDude.reset_configuration!` are available. The reset helpers mainly exist for tests and isolated tooling.
|
|
92
|
+
|
|
93
|
+
## Storage Options
|
|
94
|
+
|
|
95
|
+
| Storage | Best for | Pros | Cons |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `:file` | Local development and single-node QA | No DB migrations, easy to inspect, easy to clear | Local disk may be ephemeral; not shared across containers/dynos |
|
|
98
|
+
| `:database` | QA/staging-like environments with multiple app processes | Shared, persistent, searchable, works across nodes | Requires migration; can store sensitive content in DB; needs cleanup |
|
|
99
|
+
| `:memory` | Tests and throwaway demos | Fast, simple | Process-local, lost on restart |
|
|
100
|
+
|
|
101
|
+
## FileStore Path
|
|
102
|
+
|
|
103
|
+
The default FileStore path is `Rails.root/tmp/mail_dude`. MailDude does not default to global `/tmp/mail_dude`, because multiple Rails apps on the same machine could collide. If Rails root is unavailable, MailDude falls back to `Dir.tmpdir/mail_dude`.
|
|
104
|
+
|
|
105
|
+
FileStore under `Rails.root/tmp/mail_dude` may be wiped by deploys, container restarts, or cleanup scripts. Use `:database` or persistent shared disk for multi-node QA.
|
|
106
|
+
|
|
107
|
+
## DatabaseStore Setup
|
|
108
|
+
|
|
109
|
+
Use database storage when QA runs multiple processes, containers, or dynos:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
config.storage = :database
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then copy and run the migration:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
bin/rails generate mail_dude:install --database
|
|
119
|
+
bin/rails db:migrate
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
DatabaseStore uses `mail_dude_stored_emails` and does not require Active Storage.
|
|
123
|
+
|
|
124
|
+
## MemoryStore
|
|
125
|
+
|
|
126
|
+
MemoryStore is useful for tests and throwaway demos:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
config.storage = :memory
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
It is thread-safe but process-local and loses messages on restart.
|
|
133
|
+
|
|
134
|
+
## UI Overview
|
|
135
|
+
|
|
136
|
+
Mounting the engine exposes a mailbox UI with a message list, selected message metadata, HTML preview in a sandboxed iframe, plain text, headers, raw source, search, pagination, delete, clear, and attachment download links.
|
|
137
|
+
|
|
138
|
+
## Action Cable Live Updates
|
|
139
|
+
|
|
140
|
+
MailDude can optionally use Action Cable to show a “New message captured” banner without requiring a page reload. This is disabled by default.
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
MailDude.configure do |config|
|
|
144
|
+
config.live_updates = true
|
|
145
|
+
config.live_update_stream_name = "mail_dude:#{Rails.env}:messages"
|
|
146
|
+
config.live_update_authorizer = lambda { |connection|
|
|
147
|
+
user = connection.respond_to?(:current_user) ? connection.current_user : nil
|
|
148
|
+
user && Ability.new(user).can?(:manage, MailDude::Dashboard)
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The authorizer receives the Action Cable `connection`, not a controller. This is intentional: mounting `/mail_dude` behind a route constraint does not protect `/cable`. Your host app must expose whatever identity the authorizer needs from `ApplicationCable::Connection`.
|
|
154
|
+
|
|
155
|
+
Example host connection:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
module ApplicationCable
|
|
159
|
+
class Connection < ActionCable::Connection::Base
|
|
160
|
+
identified_by :current_user
|
|
161
|
+
|
|
162
|
+
def connect
|
|
163
|
+
self.current_user = find_verified_user
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def find_verified_user
|
|
169
|
+
env["warden"].user || reject_unauthorized_connection
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
MailDude rejects cable subscriptions when `live_updates` is false or when `live_update_authorizer` returns false. Broadcast payloads include only list metadata such as id, subject, sender, recipients, captured time, and attachment count. They do not include raw source, headers, bodies, or attachment bytes.
|
|
176
|
+
|
|
177
|
+
## Cleanup And Retention
|
|
178
|
+
|
|
179
|
+
MailDude prunes after capture using:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
config.max_messages = 1_000
|
|
183
|
+
config.retention_period = 7.days
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Set either value to `nil` to disable that pruning dimension.
|
|
187
|
+
|
|
188
|
+
## Rake Tasks
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
bin/rails mail_dude:clear
|
|
192
|
+
bin/rails mail_dude:prune
|
|
193
|
+
bin/rails mail_dude:stats
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`clear` removes all captured messages. `prune` applies the configured retention and count limits. `stats` prints the storage adapter, total count, and storage location details.
|
|
197
|
+
|
|
198
|
+
## Security Considerations
|
|
199
|
+
|
|
200
|
+
Captured emails may contain PII, password reset links, invoices, tokens, and secrets.
|
|
201
|
+
|
|
202
|
+
Do not expose `/mail_dude` publicly. Do not enable in production unless you fully understand the risk. Prefer short retention. Prefer DatabaseStore or persistent disk in multi-node QA. FileStore under `Rails.root/tmp/mail_dude` may be wiped by deploys, container restarts, or cleanup scripts.
|
|
203
|
+
|
|
204
|
+
Captured HTML is rendered in a sandboxed iframe with a restrictive Content Security Policy. Attachments are extracted from raw `.eml` data on request and filenames are sanitized.
|
|
205
|
+
|
|
206
|
+
If Action Cable live updates are enabled, protect subscriptions with `live_update_authorizer`. The `/mail_dude` route constraint does not authorize `/cable`.
|
|
207
|
+
|
|
208
|
+
## Production Warning
|
|
209
|
+
|
|
210
|
+
Production is disabled by default. If `:mail_dude` is configured in a disabled environment, delivery raises `MailDude::DisabledEnvironmentError` and does not store or send the email.
|
|
211
|
+
|
|
212
|
+
An escape hatch exists:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
config.allow_production = true
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Avoid this unless you have a reviewed operational and data-retention plan.
|
|
219
|
+
|
|
220
|
+
## Testing The Gem Locally
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
bundle install
|
|
224
|
+
bundle exec rspec
|
|
225
|
+
bundle exec rubocop
|
|
226
|
+
bundle exec rake
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The test suite uses a dummy Rails app, SQLite for DatabaseStore specs, RSpec, and SimpleCov with 100% line and branch coverage gates.
|
|
230
|
+
|
|
231
|
+
## Contributing
|
|
232
|
+
|
|
233
|
+
Keep changes small, covered, and consistent with Rails engine conventions. Do not add host-app authentication dependencies to MailDude itself.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT. See `LICENSE.txt`.
|
|
Binary file
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
.mail-dude {
|
|
2
|
+
background: #f7f8fa;
|
|
3
|
+
color: #1d2433;
|
|
4
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5
|
+
margin: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mail-dude-shell {
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.mail-dude-header {
|
|
13
|
+
align-items: center;
|
|
14
|
+
background: #ffffff;
|
|
15
|
+
border-bottom: 1px solid #d8dee8;
|
|
16
|
+
display: flex;
|
|
17
|
+
gap: 16px;
|
|
18
|
+
padding: 14px 18px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.mail-dude-brand {
|
|
22
|
+
align-items: center;
|
|
23
|
+
display: flex;
|
|
24
|
+
gap: 10px;
|
|
25
|
+
margin-right: auto;
|
|
26
|
+
min-width: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.mail-dude-logo {
|
|
30
|
+
flex: 0 0 58px;
|
|
31
|
+
height: 40px;
|
|
32
|
+
object-fit: contain;
|
|
33
|
+
width: 58px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.mail-dude-header h1 {
|
|
37
|
+
font-size: 20px;
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mail-dude-search {
|
|
42
|
+
align-items: center;
|
|
43
|
+
display: flex;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.mail-dude-search input {
|
|
48
|
+
border: 1px solid #b9c2d0;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
font: inherit;
|
|
51
|
+
padding: 7px 9px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.mail-dude-search input[type="submit"],
|
|
55
|
+
.mail-dude-danger {
|
|
56
|
+
border: 1px solid #a91f2f;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
background: #ffffff;
|
|
59
|
+
color: #8a1422;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
font: inherit;
|
|
62
|
+
padding: 7px 10px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mail-dude-layout {
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-columns: minmax(260px, 34%) 1fr;
|
|
68
|
+
min-height: calc(100vh - 65px);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mail-dude-list {
|
|
72
|
+
background: #ffffff;
|
|
73
|
+
border-right: 1px solid #d8dee8;
|
|
74
|
+
overflow: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.mail-dude-list ul {
|
|
78
|
+
list-style: none;
|
|
79
|
+
margin: 0;
|
|
80
|
+
padding: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.mail-dude-list-item {
|
|
84
|
+
border-bottom: 1px solid #e6ebf2;
|
|
85
|
+
color: inherit;
|
|
86
|
+
display: grid;
|
|
87
|
+
gap: 3px;
|
|
88
|
+
padding: 12px 14px;
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.mail-dude-list-item.is-active {
|
|
93
|
+
background: #e8f2ff;
|
|
94
|
+
box-shadow: inset 4px 0 0 #1c68d1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.mail-dude-list-item span {
|
|
98
|
+
color: #596579;
|
|
99
|
+
font-size: 13px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.mail-dude-pane {
|
|
103
|
+
padding: 18px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.mail-dude-pane-header {
|
|
107
|
+
align-items: center;
|
|
108
|
+
display: flex;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
gap: 16px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.mail-dude-pane-header h2 {
|
|
114
|
+
font-size: 22px;
|
|
115
|
+
margin: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.mail-dude-metadata {
|
|
119
|
+
display: grid;
|
|
120
|
+
grid-template-columns: 130px 1fr;
|
|
121
|
+
margin: 18px 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.mail-dude-metadata dt,
|
|
125
|
+
.mail-dude-metadata dd {
|
|
126
|
+
border-top: 1px solid #d8dee8;
|
|
127
|
+
margin: 0;
|
|
128
|
+
padding: 8px 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.mail-dude-metadata dt {
|
|
132
|
+
color: #596579;
|
|
133
|
+
font-weight: 700;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.mail-dude-tabs {
|
|
137
|
+
display: flex;
|
|
138
|
+
gap: 10px;
|
|
139
|
+
margin-bottom: 10px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.mail-dude-message-html-frame {
|
|
143
|
+
background: #ffffff;
|
|
144
|
+
border: 1px solid #d8dee8;
|
|
145
|
+
border-radius: 6px;
|
|
146
|
+
min-height: 420px;
|
|
147
|
+
width: 100%;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.mail-dude-empty,
|
|
151
|
+
.mail-dude-error,
|
|
152
|
+
.mail-dude-live-banner,
|
|
153
|
+
.mail-dude-notice {
|
|
154
|
+
padding: 18px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.mail-dude-live-banner {
|
|
158
|
+
background: #e8f2ff;
|
|
159
|
+
border-bottom: 1px solid #9bbce8;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.mail-dude-pagination {
|
|
163
|
+
align-items: center;
|
|
164
|
+
display: flex;
|
|
165
|
+
gap: 12px;
|
|
166
|
+
padding: 12px 14px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (max-width: 800px) {
|
|
170
|
+
.mail-dude-header,
|
|
171
|
+
.mail-dude-layout {
|
|
172
|
+
display: block;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.mail-dude-list {
|
|
176
|
+
border-bottom: 1px solid #d8dee8;
|
|
177
|
+
border-right: 0;
|
|
178
|
+
max-height: 45vh;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailDude
|
|
4
|
+
class MessagesChannel < ActionCable::Channel::Base
|
|
5
|
+
def subscribed
|
|
6
|
+
return reject unless authorized?
|
|
7
|
+
|
|
8
|
+
stream_from MailDude.configuration.live_update_stream_name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def authorized?
|
|
14
|
+
MailDude.configuration.live_updates &&
|
|
15
|
+
MailDude.configuration.live_update_authorizer.call(connection)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailDude
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
layout 'mail_dude/application'
|
|
7
|
+
|
|
8
|
+
rescue_from AttachmentNotFoundError, MessageNotFoundError, with: :render_not_found
|
|
9
|
+
rescue_from InvalidConfigurationError, StorageError, with: :render_storage_error
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def render_not_found
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html { render 'mail_dude/messages/error', status: :not_found, locals: { message: 'Message not found.' } }
|
|
16
|
+
format.any { head :not_found }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_storage_error(error)
|
|
21
|
+
diagnostic = Rails.env.production? ? 'MailDude storage is unavailable.' : error.message
|
|
22
|
+
render 'mail_dude/messages/error', status: :internal_server_error, locals: { message: diagnostic }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailDude
|
|
4
|
+
class AttachmentsController < ApplicationController
|
|
5
|
+
SAFE_INLINE_TYPES = %w[image/gif image/jpeg image/png image/webp image/svg+xml].freeze
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
record = MailDude.store.find(params[:message_id] || params[:id])
|
|
9
|
+
attachment = AttachmentLocator.new(record).find(params[:attachment_id])
|
|
10
|
+
send_data attachment.data,
|
|
11
|
+
filename: attachment.filename,
|
|
12
|
+
type: attachment.content_type,
|
|
13
|
+
disposition: disposition_for(attachment)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def disposition_for(attachment)
|
|
19
|
+
if params[:inline] == '1' && attachment.inline? && SAFE_INLINE_TYPES.include?(attachment.content_type)
|
|
20
|
+
return 'inline'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
'attachment'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailDude
|
|
4
|
+
class MessagesController < ApplicationController
|
|
5
|
+
before_action :load_page, only: %i[index show]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@selected_message = first_message
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
@selected_message = MailDude.store.find(params[:id])
|
|
13
|
+
render :index
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def html
|
|
17
|
+
presenter = presenter_for(params[:id])
|
|
18
|
+
response.headers['Content-Security-Policy'] =
|
|
19
|
+
"default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; font-src data:; " \
|
|
20
|
+
"base-uri 'none'; form-action 'none'; script-src 'none'"
|
|
21
|
+
render html: renderer_for(presenter).render.html_safe, layout: false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def text
|
|
25
|
+
render plain: presenter_for(params[:id]).text_body.presence || 'This message does not include a plain text body.'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def headers
|
|
29
|
+
render plain: presenter_for(params[:id]).raw_headers.presence || 'This message does not include headers.'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raw
|
|
33
|
+
record = MailDude.store.find(params[:id])
|
|
34
|
+
response.headers['Content-Disposition'] = %(inline; filename="#{record.id}.eml")
|
|
35
|
+
render plain: record.raw_source, content_type: 'text/plain'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def destroy
|
|
39
|
+
raise MessageNotFoundError, 'Message not found' unless MailDude.store.delete(params[:id])
|
|
40
|
+
|
|
41
|
+
redirect_to messages_path, notice: 'Message deleted.'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear
|
|
45
|
+
count = MailDude.store.clear
|
|
46
|
+
redirect_to messages_path, notice: "#{count} messages cleared."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def first_message
|
|
52
|
+
first_record = @page.records.first
|
|
53
|
+
first_record ? MailDude.store.find(first_record.id) : nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_page
|
|
57
|
+
@query = params[:q]
|
|
58
|
+
@page = MailDude.store.list(page: params[:page], per_page: params[:per_page], query: @query)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def presenter_for(id)
|
|
62
|
+
MessagePresenter.new(MailDude.store.find(id))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def renderer_for(presenter)
|
|
66
|
+
HtmlBodyRenderer.new(presenter,
|
|
67
|
+
attachment_url: lambda do |attachment_id, **|
|
|
68
|
+
attachment_message_path(presenter.id, attachment_id, inline: '1')
|
|
69
|
+
end)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MailDude
|
|
4
|
+
module ApplicationHelper
|
|
5
|
+
def mail_dude_address_list(values)
|
|
6
|
+
Array(values).presence&.join(', ') || 'None'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def mail_dude_selected?(message)
|
|
10
|
+
@selected_message&.id == message.id
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|