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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +237 -0
  5. data/app/assets/images/mail_dude/icon.png +0 -0
  6. data/app/assets/stylesheets/mail_dude/application.css +180 -0
  7. data/app/channels/mail_dude/messages_channel.rb +18 -0
  8. data/app/controllers/mail_dude/application_controller.rb +25 -0
  9. data/app/controllers/mail_dude/attachments_controller.rb +26 -0
  10. data/app/controllers/mail_dude/messages_controller.rb +72 -0
  11. data/app/helpers/mail_dude/application_helper.rb +13 -0
  12. data/app/models/mail_dude/application_record.rb +7 -0
  13. data/app/models/mail_dude/stored_email.rb +7 -0
  14. data/app/views/layouts/mail_dude/application.html.erb +134 -0
  15. data/app/views/mail_dude/messages/_empty.html.erb +4 -0
  16. data/app/views/mail_dude/messages/_error.html.erb +5 -0
  17. data/app/views/mail_dude/messages/_list.html.erb +44 -0
  18. data/app/views/mail_dude/messages/_message_pane.html.erb +30 -0
  19. data/app/views/mail_dude/messages/_metadata.html.erb +33 -0
  20. data/app/views/mail_dude/messages/_tabs.html.erb +14 -0
  21. data/app/views/mail_dude/messages/error.html.erb +4 -0
  22. data/app/views/mail_dude/messages/index.html.erb +17 -0
  23. data/app/views/mail_dude/messages/show.html.erb +2 -0
  24. data/config/routes.rb +19 -0
  25. data/db/migrate/20260506170200_create_mail_dude_stored_emails.rb +36 -0
  26. data/lib/generators/mail_dude/install_generator.rb +49 -0
  27. data/lib/generators/mail_dude/templates/create_mail_dude_stored_emails.tt +37 -0
  28. data/lib/generators/mail_dude/templates/initializer.tt +17 -0
  29. data/lib/mail_dude/attachment_locator.rb +111 -0
  30. data/lib/mail_dude/configuration.rb +102 -0
  31. data/lib/mail_dude/dashboard.rb +6 -0
  32. data/lib/mail_dude/delivery_method.rb +46 -0
  33. data/lib/mail_dude/engine.rb +24 -0
  34. data/lib/mail_dude/errors.rb +11 -0
  35. data/lib/mail_dude/html_body_renderer.rb +49 -0
  36. data/lib/mail_dude/mailer_metadata_headers.rb +22 -0
  37. data/lib/mail_dude/message_broadcast.rb +50 -0
  38. data/lib/mail_dude/message_presenter.rb +136 -0
  39. data/lib/mail_dude/message_record.rb +17 -0
  40. data/lib/mail_dude/message_serializer.rb +117 -0
  41. data/lib/mail_dude/pagination.rb +35 -0
  42. data/lib/mail_dude/stores/base.rb +106 -0
  43. data/lib/mail_dude/stores/database_store.rb +93 -0
  44. data/lib/mail_dude/stores/file_store.rb +126 -0
  45. data/lib/mail_dude/stores/memory_store.rb +53 -0
  46. data/lib/mail_dude/version.rb +5 -0
  47. data/lib/mail_dude.rb +80 -0
  48. data/lib/tasks/mail_dude.rake +25 -0
  49. 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
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial MailDude Rails engine implementation.
6
+
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`.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailDude
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailDude
4
+ class StoredEmail < ApplicationRecord
5
+ self.table_name = 'mail_dude_stored_emails'
6
+ end
7
+ end