rails_mail 0.9.3 → 0.10.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 +4 -4
- data/README.md +17 -3
- data/app/controllers/rails_mail/emails_controller.rb +13 -4
- data/app/frontend/rails_mail/application.js +4 -2
- data/app/frontend/rails_mail/modules/email_highlight_controller.js +8 -7
- data/app/frontend/rails_mail/modules/redirect_controller.js +12 -0
- data/app/models/rails_mail/email.rb +30 -8
- data/app/views/layouts/rails_mail/_head.html.erb +16 -0
- data/app/views/layouts/rails_mail/_title.html.erb +1 -0
- data/app/views/layouts/rails_mail/application.html.erb +31 -28
- data/app/views/rails_mail/emails/_email_tabs.html.erb +28 -0
- data/app/views/rails_mail/emails/destroy.turbo_stream.erb +19 -0
- data/app/views/rails_mail/emails/destroy_all.turbo_stream.erb +3 -1
- data/app/views/rails_mail/emails/index.turbo_stream.erb +1 -1
- data/app/views/rails_mail/emails/show.html.erb +5 -4
- data/app/views/rails_mail/shared/_email.html.erb +37 -19
- data/app/views/rails_mail/shared/_email_sidebar.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/lib/generators/rails_mail/install/templates/initializer.rb +2 -2
- data/lib/rails_mail/configuration.rb +6 -6
- data/lib/rails_mail/delivery_method.rb +13 -1
- data/lib/rails_mail/version.rb +1 -1
- data/lib/rails_mail.rb +4 -16
- metadata +8 -4
- data/app/views/rails_mail/emails/_show.html.erb +0 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74639c1eeda1941950ff03fed49be529f335fc3e1e6e9896afd8ce9a636daaa6
|
4
|
+
data.tar.gz: bd46e033763529b995f444ef78d0390404d088d816e46300cf59bec3efbf3719
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d1899db5ec6f5d1373c50e3dcc737be3a0156c01bbb60c8291e32a9ca5a8f0902451c7c90478ff4967624d1e372e842862067e9229ae634757f2518f311f557
|
7
|
+
data.tar.gz: 24845fe1ef0b59edcc0257f0b1cf695b2e5872b38d0dd8f5d5afb1989401684cc4e2aa0e5764ea74f90ec4279a829443ee42e6cde6eb9ec84a7d65e321f727bc
|
data/README.md
CHANGED
@@ -15,9 +15,10 @@ RailsMail saves all outgoing emails to your database instead of actually sending
|
|
15
15
|
* Clean, responsive UI for viewing email contents
|
16
16
|
* Optional authentication support
|
17
17
|
* Trimming emails older than a specified duration or a maximum number of emails
|
18
|
-
* Ability to manually clear emails
|
18
|
+
* Ability to manually clear emails in bulk or individually. The bulk delete can be turned on/off based on environment (eg, so that in Staging, other stakeholders can't clear emails out, but in dev sometimes you want a clean slate)
|
19
19
|
* Dynamic time ago in words using date-fns
|
20
20
|
* Ability to customize how the job that trims emails is enqueued
|
21
|
+
* Ability to customize the title in the top left of the page via a standard Rails view that overrides the engine's default view.
|
21
22
|
|
22
23
|
## Installation
|
23
24
|
|
@@ -79,7 +80,7 @@ To use RailsMail in your application:
|
|
79
80
|
3. **Configure the initializer**
|
80
81
|
See the [Configuration](#configuration) section for more details.
|
81
82
|
|
82
|
-
4. **Visit `/rails_mail` in your browser to view all captured emails.**
|
83
|
+
4. **Visit `/rails_mail` (or where ever you mounted the engine) in your browser to view all captured emails.**
|
83
84
|
|
84
85
|
### Configuration
|
85
86
|
|
@@ -90,11 +91,17 @@ RailsMail can be configured through an initializer:
|
|
90
91
|
RailsMail.configure do |config|
|
91
92
|
# Optional authentication callback
|
92
93
|
# (if using Authlogic. If using Devise see the Authentication section)
|
93
|
-
config.
|
94
|
+
config.authenticate do
|
94
95
|
user_session = UserSession.find
|
95
96
|
raise ActionController::RoutingError.new('Not Found') unless user_session&.user&.admin?
|
96
97
|
end
|
97
98
|
|
99
|
+
# Optional decide whether to show the clear button
|
100
|
+
# Useful if you want devs in local envs to be able to clear all emails, but not in staging
|
101
|
+
config.show_clear_all_button do
|
102
|
+
Rails.env.development?
|
103
|
+
end
|
104
|
+
|
98
105
|
# Delete emails older than the specified duration
|
99
106
|
config.trim_emails_older_than = 30.days
|
100
107
|
|
@@ -113,6 +120,13 @@ end
|
|
113
120
|
- `trim_emails_max_count`: Keeps only the N most recent emails, deleting older ones.
|
114
121
|
- `sync_via`: Controls whether the trimming job runs synchronously (:now) or asynchronously (:later)
|
115
122
|
|
123
|
+
### Customize the title
|
124
|
+
Since this is a Rails engine, you can customize the title by creating a file at `app/views/layouts/rails_mail/_title.html.erb`.
|
125
|
+
|
126
|
+
```erb
|
127
|
+
<h1 class="text-xl font-bold">My apps mails</h1>
|
128
|
+
```
|
129
|
+
|
116
130
|
## Authentication
|
117
131
|
Authentication is optional, but recommended and will depend on your application's authentication setup. This gem provides an `authentication_callback` that you can configure in the initializer which is helpful for Authlogic. If you are using Devise, you can simply wrap the mount point of the engine.
|
118
132
|
|
@@ -17,10 +17,19 @@ module RailsMail
|
|
17
17
|
def show
|
18
18
|
@emails = Email.order(created_at: :desc)
|
19
19
|
@email = Email.find(params[:id])
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
session[:current_email_id] = @email.id
|
21
|
+
|
22
|
+
render :show
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy
|
26
|
+
@email = Email.find(params[:id])
|
27
|
+
@current_email_id = session[:current_email_id]
|
28
|
+
@email.destroy
|
29
|
+
|
30
|
+
respond_to do |format|
|
31
|
+
format.html { redirect_to emails_path }
|
32
|
+
format.turbo_stream
|
24
33
|
end
|
25
34
|
end
|
26
35
|
|
@@ -1,14 +1,16 @@
|
|
1
1
|
import { Application } from "stimulus";
|
2
2
|
import "turbo";
|
3
3
|
import EmailHighlightController from "email_highlight_controller";
|
4
|
+
import RedirectController from "redirect_controller";
|
4
5
|
import AutoSubmit from 'auto_submit'
|
5
6
|
import Timeago from 'timeago'
|
6
7
|
|
7
8
|
const application = Application.start();
|
8
9
|
|
9
10
|
application.register("email-highlight", EmailHighlightController);
|
10
|
-
application.register(
|
11
|
-
application.register('
|
11
|
+
application.register("redirect", RedirectController);
|
12
|
+
application.register('auto-submit', AutoSubmit);
|
13
|
+
application.register('timeago', Timeago);
|
12
14
|
|
13
15
|
window.Stimulus = Application.start();
|
14
16
|
|
@@ -19,14 +19,14 @@ export default class extends Controller {
|
|
19
19
|
|
20
20
|
// Called when a link is clicked
|
21
21
|
highlight(event) {
|
22
|
-
// Remove active class from all
|
22
|
+
// Remove active class from all email containers
|
23
23
|
this.linkTargets.forEach(link => {
|
24
|
-
link.classList.remove(this.constructor.activeClass)
|
24
|
+
link.closest(".group").classList.remove(this.constructor.activeClass)
|
25
25
|
})
|
26
26
|
|
27
|
-
// Add active class to clicked
|
27
|
+
// Add active class to clicked email's container
|
28
28
|
const clickedLink = event.currentTarget
|
29
|
-
clickedLink.classList.add(this.constructor.activeClass)
|
29
|
+
clickedLink.closest(".group").classList.add(this.constructor.activeClass)
|
30
30
|
}
|
31
31
|
|
32
32
|
highlightCurrentEmail() {
|
@@ -35,12 +35,13 @@ export default class extends Controller {
|
|
35
35
|
if (emailContent) {
|
36
36
|
const currentEmailId = emailContent.dataset.emailId
|
37
37
|
|
38
|
-
// Highlight the
|
38
|
+
// Highlight the container with the matching email ID
|
39
39
|
this.linkTargets.forEach(link => {
|
40
|
+
const container = link.closest(".group")
|
40
41
|
if (link.dataset.emailId === currentEmailId) {
|
41
|
-
|
42
|
+
container.classList.add(this.constructor.activeClass)
|
42
43
|
} else {
|
43
|
-
|
44
|
+
container.classList.remove(this.constructor.activeClass)
|
44
45
|
}
|
45
46
|
})
|
46
47
|
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { Controller } from 'stimulus'
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static values = { url: String }
|
5
|
+
connect () {
|
6
|
+
// Need to remove the element
|
7
|
+
// Otherwise, if we revisit this page using a Turbo page cache
|
8
|
+
// it may end up redirecting again
|
9
|
+
this.element.remove();
|
10
|
+
Turbo.visit(this.urlValue)
|
11
|
+
}
|
12
|
+
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module RailsMail
|
2
2
|
class Email < ApplicationRecord
|
3
|
-
|
3
|
+
include RailsMail::Engine.routes.url_helpers
|
4
|
+
store_accessor :data, :from, :to, :cc, :bcc, :subject, :html_part, :text_part, :content_type, :attachments
|
4
5
|
|
5
6
|
validates :from, presence: true
|
6
7
|
validates :to, presence: true
|
@@ -13,24 +14,45 @@ module RailsMail
|
|
13
14
|
}
|
14
15
|
|
15
16
|
def text?
|
16
|
-
content_type&.include?("text/plain")
|
17
|
+
content_type&.include?("text/plain") || content_type&.include?("multipart/alternative")
|
17
18
|
end
|
18
19
|
|
19
20
|
def html?
|
20
|
-
content_type&.include?("text/html")
|
21
|
+
content_type&.include?("text/html") || content_type&.include?("multipart/alternative")
|
22
|
+
end
|
23
|
+
|
24
|
+
def next_email
|
25
|
+
RailsMail::Email.where("id < ?", id).last || RailsMail::Email.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def html_body
|
29
|
+
return nil unless html?
|
30
|
+
|
31
|
+
html_part["raw_source"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def text_body
|
35
|
+
return nil unless text?
|
36
|
+
|
37
|
+
text_part["raw_source"]
|
21
38
|
end
|
22
39
|
|
23
40
|
private
|
24
41
|
|
25
42
|
def broadcast_email
|
26
|
-
return unless defined?(::
|
43
|
+
return unless defined?(::ActionCable)
|
27
44
|
|
28
|
-
|
29
|
-
"rails_mail:emails",
|
30
|
-
target: "email-sidebar",
|
45
|
+
html = ApplicationController.render(
|
31
46
|
partial: "rails_mail/shared/email",
|
32
|
-
locals: { email: self }
|
47
|
+
locals: { email: self, email_path: email_path(self) }
|
48
|
+
)
|
49
|
+
|
50
|
+
turbo_stream = RailsMail::TurboHelper::TurboStreamBuilder.new.prepend(
|
51
|
+
target: "email-sidebar",
|
52
|
+
content: html
|
33
53
|
)
|
54
|
+
|
55
|
+
ActionCable.server.broadcast("rails_mail:emails", turbo_stream)
|
34
56
|
rescue StandardError => e
|
35
57
|
Rails.logger.error "RailsMail::Email#broadcast_email failed: #{e.message}"
|
36
58
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
<head>
|
3
|
+
<title>RailsMail - <%= yield(:title) if content_for?(:title) %></title>
|
4
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
5
|
+
<%= action_cable_meta_tag if defined?(::ActionCable) %>
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
<%= csp_meta_tag %>
|
8
|
+
<%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, v: RailsMail::VERSION, locale: nil), nonce: content_security_policy_nonce %>
|
9
|
+
|
10
|
+
<%= tag.script "", src: frontend_static_path(:tailwind, format: :js, v: RailsMail::VERSION, locale: nil), nonce: content_security_policy_nonce %>
|
11
|
+
|
12
|
+
<% importmaps = RailsMail::FrontendsController.js_modules.keys.index_with { |module_name| frontend_module_path(module_name, format: :js, locale: nil) } %>
|
13
|
+
<%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %>
|
14
|
+
<%= tag.script "", type: "module", nonce: content_security_policy_nonce do %> import "application"; <% end %>
|
15
|
+
|
16
|
+
</head>
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1 class="text-xl font-bold">Rails Mail</h1>
|
@@ -1,34 +1,37 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html>
|
3
|
-
|
4
|
-
<title>RailsMail - <%= yield(:title) if content_for?(:title) %></title>
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
-
<%= action_cable_meta_tag if defined?(::ActionCable) %>
|
7
|
-
<%= csrf_meta_tags %>
|
8
|
-
<%= csp_meta_tag %>
|
9
|
-
<%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, v: RailsMail::VERSION, locale: nil), nonce: content_security_policy_nonce %>
|
10
|
-
|
11
|
-
<%= tag.script "", src: frontend_static_path(:tailwind, format: :js, v: RailsMail::VERSION, locale: nil), nonce: content_security_policy_nonce %>
|
3
|
+
<%= render "layouts/rails_mail/head" %>
|
12
4
|
|
13
|
-
|
14
|
-
|
15
|
-
|
5
|
+
<body class="bg-gray-100">
|
6
|
+
<div class="flex h-screen relative">
|
7
|
+
<!-- Mobile menu checkbox (hidden) -->
|
8
|
+
<input type="checkbox" class="hidden peer" id="sidebar-toggle">
|
16
9
|
|
17
|
-
|
10
|
+
<!-- Collapsed menu button -->
|
11
|
+
<label for="sidebar-toggle" class="lg:hidden fixed top-4 left-4 z-20 p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 cursor-pointer peer-checked:hidden bg-gray-200">
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
13
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
14
|
+
</svg>
|
15
|
+
</label>
|
18
16
|
|
19
|
-
<body class="bg-gray-100">
|
20
|
-
<div class="flex h-screen">
|
21
17
|
<!-- Sidebar -->
|
22
|
-
<div class="w-80 bg-white border-r border-gray-200 flex flex-col">
|
18
|
+
<div class="fixed lg:static inset-y-0 left-0 transform -translate-x-full peer-checked:translate-x-0 lg:translate-x-0 transition duration-200 ease-in-out w-80 bg-white border-r border-gray-200 flex flex-col z-10">
|
23
19
|
<div class="flex-none p-4">
|
24
|
-
<div class="flex items-center justify-between">
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
20
|
+
<div class="flex items-center justify-between w-full">
|
21
|
+
<!-- Menu icon (visible only on mobile) -->
|
22
|
+
<label for="sidebar-toggle" class="lg:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 cursor-pointer bg-gray-200">
|
23
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
24
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
25
|
+
</svg>
|
26
|
+
</label>
|
27
|
+
|
28
|
+
<!-- Title (centered) -->
|
29
|
+
<%= link_to root_path, class: "text-gray-900" do %>
|
30
|
+
<%= render "layouts/rails_mail/title" %>
|
31
|
+
<% end %>
|
30
32
|
|
31
|
-
|
33
|
+
<!-- Clear All button (or empty div for spacing when button is hidden) -->
|
34
|
+
<% if instance_eval(&RailsMail.show_clear_all_button_callback) %>
|
32
35
|
<%= button_to destroy_all_emails_path,
|
33
36
|
class: "text-sm text-gray-600 hover:text-gray-900",
|
34
37
|
form: { data: { turbo_confirm: "Are you sure you want to clear all emails?" } },
|
@@ -40,6 +43,8 @@
|
|
40
43
|
Clear All
|
41
44
|
</div>
|
42
45
|
<% end %>
|
46
|
+
<% else %>
|
47
|
+
<div class="w-[88px]"></div> <!-- Placeholder to maintain spacing when button is hidden -->
|
43
48
|
<% end %>
|
44
49
|
</div>
|
45
50
|
</div>
|
@@ -60,11 +65,9 @@
|
|
60
65
|
</div>
|
61
66
|
|
62
67
|
<!-- Main content -->
|
63
|
-
<div class="flex-1 overflow-y-auto">
|
64
|
-
<div class="p-8">
|
65
|
-
|
66
|
-
<%= yield %>
|
67
|
-
</turbo-frame>
|
68
|
+
<div class="flex-1 overflow-y-auto lg:ml-0">
|
69
|
+
<div class="p-8 lg:p-8 sm:p-4">
|
70
|
+
<%= yield %>
|
68
71
|
</div>
|
69
72
|
</div>
|
70
73
|
</div>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<div class="prose max-w-none break-words">
|
2
|
+
<input type="radio" name="email_tab" id="html_tab" class="hidden peer/html" <%= email.html? ? 'checked' : '' %> <%= email.html? ? '' : 'disabled' %> />
|
3
|
+
<label
|
4
|
+
for="html_tab"
|
5
|
+
class="px-4 py-2 text-sm font-medium cursor-pointer peer-checked/html:text-blue-600 peer-checked/html:border-blue-600 peer-checked/html:border-b-2 peer-disabled/html:text-gray-400 peer-disabled/html:cursor-not-allowed">
|
6
|
+
HTML
|
7
|
+
</label>
|
8
|
+
|
9
|
+
<input type="radio" name="email_tab" id="text_tab" class="hidden peer/text" <%= email.text? ? (!email.html? ? 'checked' : '') : 'disabled' %> />
|
10
|
+
<label
|
11
|
+
for="text_tab"
|
12
|
+
class="px-4 py-2 text-sm font-medium cursor-pointer peer-checked/text:text-blue-600 peer-checked/text:border-blue-600 peer-checked/text:border-b-2 peer-disabled/text:text-gray-400 peer-disabled/text:cursor-not-allowed">
|
13
|
+
Text
|
14
|
+
</label>
|
15
|
+
|
16
|
+
<div class="hidden peer-checked/html:block mt-3">
|
17
|
+
<% if email.html? %>
|
18
|
+
<%= sanitize(email.html_body,
|
19
|
+
tags: ActionView::Base.sanitized_allowed_tags + ['table', 'tbody', 'tr', 'td'],
|
20
|
+
attributes: ActionView::Base.sanitized_allowed_attributes + ['style']) %>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
23
|
+
<div class="hidden peer-checked/text:block mt-3 bg-black text-white font-mono p-4 rounded">
|
24
|
+
<% if email.text? %>
|
25
|
+
<%= simple_format email.text_body %>
|
26
|
+
<% end %>
|
27
|
+
</div>
|
28
|
+
</div>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<turbo-stream action="remove" target="<%= dom_id(@email) %>">
|
2
|
+
</turbo-stream>
|
3
|
+
|
4
|
+
<% path_to_visit = if RailsMail::Email.count.zero?
|
5
|
+
root_path
|
6
|
+
else
|
7
|
+
email_path(@email.next_email)
|
8
|
+
end
|
9
|
+
%>
|
10
|
+
|
11
|
+
<turbo-stream action="update" target="email_content">
|
12
|
+
<template>
|
13
|
+
<div class="" data-controller="redirect" data-redirect-url-value="<%= path_to_visit %>">
|
14
|
+
<div class="flex justify-center items-center min-h-screen">
|
15
|
+
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
</template>
|
19
|
+
</turbo-stream>
|
@@ -4,6 +4,8 @@
|
|
4
4
|
</turbo-stream>
|
5
5
|
<turbo-stream action="update" target="email_content">
|
6
6
|
<template>
|
7
|
-
|
7
|
+
<div data-controller="redirect" data-redirect-url-value="<%= emails_path %>">
|
8
|
+
<%= render(partial: "rails_mail/emails/empty_state", formats: [ :html ]) %>
|
9
|
+
</div>
|
8
10
|
</template>
|
9
11
|
</turbo-stream>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<turbo-stream action="update" target="email-sidebar">
|
2
2
|
<template>
|
3
3
|
<% @emails&.each do |email| %>
|
4
|
-
<%= render partial: "rails_mail/shared/email", locals: {email: email}, formats: [:html] %>
|
4
|
+
<%= render partial: "rails_mail/shared/email", locals: {email: email, email_path: email_path(email) }, formats: [:html] %>
|
5
5
|
<% end %>
|
6
6
|
</template>
|
7
7
|
</turbo-stream>
|
@@ -1,7 +1,8 @@
|
|
1
1
|
<%= rails_mail_turbo_frame_tag "email_content" do %>
|
2
|
-
<div class="bg-white shadow rounded-lg"
|
2
|
+
<div class="bg-white shadow rounded-lg" data-email-id="<%= @email.id %>"
|
3
|
+
data-email-highlight-current-id-value="<%= @email.id %>">
|
3
4
|
<div class="px-6 py-4 border-b border-gray-200">
|
4
|
-
<h1 class="text-2xl font-semibold text-gray-900"><%= @email.subject %></h1>
|
5
|
+
<h1 class="text-2xl font-semibold text-gray-900 break-words"><%= @email.subject %></h1>
|
5
6
|
<div class="mt-2 text-sm text-gray-600">
|
6
7
|
<div>From: <%= @email.from %></div>
|
7
8
|
<div>To: <%= @email.to.join(", ") %></div>
|
@@ -35,8 +36,8 @@
|
|
35
36
|
</div>
|
36
37
|
<% end %>
|
37
38
|
|
38
|
-
<div
|
39
|
-
<%=
|
39
|
+
<div>
|
40
|
+
<%= render "email_tabs", email: @email %>
|
40
41
|
</div>
|
41
42
|
</div>
|
42
43
|
</div>
|
@@ -2,24 +2,42 @@
|
|
2
2
|
on_current_page = current_page?(RailsMail::Engine.routes.url_helpers.email_path(email))
|
3
3
|
highlight_class = on_current_page ? 'bg-gray-300' : ''
|
4
4
|
%>
|
5
|
-
<%=
|
6
|
-
|
5
|
+
<%= content_tag :div, id: dom_id(email), class: "group flex items-center px-3 py-2 hover:bg-gray-200 #{highlight_class} active:bg-gray-300 focus:bg-gray-300", data: {testid: "email-row"} do %>
|
6
|
+
<%= link_to RailsMail::Engine.routes.url_helpers.email_path(email),
|
7
|
+
class: "flex-1 min-w-0 rounded-md",
|
8
|
+
data: {
|
9
|
+
"turbo-frame": "email_content",
|
10
|
+
"turbo-action": "advance",
|
11
|
+
"turbo-prefetch": "false",
|
12
|
+
"email-highlight-target": "link",
|
13
|
+
"email-id": email.id,
|
14
|
+
action: "click->email-highlight#highlight"
|
15
|
+
} do %>
|
16
|
+
<div class="min-w-0">
|
17
|
+
<div class="text-sm font-medium text-gray-900 truncate"><%= email.subject %></div>
|
18
|
+
<div class="text-xs text-gray-500">
|
19
|
+
<span class="truncate"><%= email.from %></span>
|
20
|
+
<time
|
21
|
+
data-controller="timeago"
|
22
|
+
data-timeago-datetime-value="<%= email.created_at.iso8601 %>"
|
23
|
+
data-timeago-refresh-interval-value="1000"
|
24
|
+
data-timeago-include-seconds-value="true"
|
25
|
+
class="ml-2 text-gray-400"><%= time_ago_in_words(email.created_at) %> ago</time>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
<% end %>
|
29
|
+
|
30
|
+
<%= button_to email_path,
|
31
|
+
method: :delete,
|
32
|
+
class: "hidden group-hover:block p-1 hover:bg-gray-300 rounded",
|
33
|
+
form: {
|
7
34
|
data: {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
<div class="text-xs text-gray-500">
|
17
|
-
<span class="truncate"><%= email.from %></span>
|
18
|
-
<time
|
19
|
-
data-controller="timeago"
|
20
|
-
data-timeago-datetime-value="<%= email.created_at.iso8601 %>"
|
21
|
-
data-timeago-refresh-interval-value="1000"
|
22
|
-
data-timeago-include-seconds-value="true"
|
23
|
-
class="ml-2 text-gray-400"><%= time_ago_in_words(email.created_at) %> ago</time>
|
24
|
-
</div>
|
35
|
+
turbo_confirm: "Are you sure you want to delete this email?",
|
36
|
+
turbo_frame: "_top"
|
37
|
+
},
|
38
|
+
} do %>
|
39
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-500 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
40
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
41
|
+
</svg>
|
42
|
+
<% end %>
|
25
43
|
<% end %>
|
data/config/routes.rb
CHANGED
@@ -4,7 +4,7 @@ RailsMail::Engine.routes.draw do
|
|
4
4
|
get "static/:name", action: :static, as: :frontend_static, constraints: { format: %w[css js] }
|
5
5
|
end
|
6
6
|
|
7
|
-
resources :emails, only: [ :index, :show ] do
|
7
|
+
resources :emails, only: [ :index, :show, :destroy ] do
|
8
8
|
collection do
|
9
9
|
delete "emails/destroy_all", to: "emails#destroy_all", as: :destroy_all
|
10
10
|
end
|
@@ -4,14 +4,14 @@ Rails.configuration.to_prepare do
|
|
4
4
|
RailsMail.configure do |config|
|
5
5
|
# Authentication setup
|
6
6
|
# if left blank, authentication is skipped
|
7
|
-
# config.
|
7
|
+
# config.authenticate do
|
8
8
|
# Example implementation for Authlogic:
|
9
9
|
# user_session = UserSession.find
|
10
10
|
# msg = Rails.env.development? ? 'Forbidden - make sure you have the correct permission in config/initializers/rails_mail.rb' : 'Not Found'
|
11
11
|
# raise ActionController::RoutingError.new(msg) unless user_session&.user&.admin?
|
12
12
|
# end
|
13
13
|
|
14
|
-
config.
|
14
|
+
config.show_clear_all_button do
|
15
15
|
# show clear button in development
|
16
16
|
# and prefer to trim in non-development environments
|
17
17
|
# to prevent accidental deletion of emails
|
@@ -1,29 +1,29 @@
|
|
1
1
|
module RailsMail
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :authentication_callback, :
|
3
|
+
attr_accessor :authentication_callback, :show_clear_all_button_callback,
|
4
4
|
:trim_emails_older_than, :trim_emails_max_count,
|
5
5
|
:enqueue_trim_job
|
6
6
|
|
7
7
|
def initialize
|
8
8
|
@authentication_callback = nil
|
9
|
-
@
|
9
|
+
@show_clear_all_button_callback = nil
|
10
10
|
@trim_emails_older_than = nil
|
11
11
|
@trim_emails_max_count = nil
|
12
12
|
@enqueue_trim_job = ->(email) { RailsMail::TrimEmailsJob.perform_later }
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
15
|
+
def authenticate(&callback)
|
16
16
|
unless callback.nil? || callback.respond_to?(:call)
|
17
17
|
raise ArgumentError, "authentication_callback must be nil or respond to #call"
|
18
18
|
end
|
19
19
|
@authentication_callback = callback
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
22
|
+
def show_clear_all_button(&callback)
|
23
23
|
unless callback.nil? || callback.respond_to?(:call)
|
24
|
-
raise ArgumentError, "
|
24
|
+
raise ArgumentError, "show_clear_all_button must be nil or respond to #call"
|
25
25
|
end
|
26
|
-
@
|
26
|
+
@show_clear_all_button_callback = callback
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
@@ -10,7 +10,8 @@ module RailsMail
|
|
10
10
|
cc: mail.cc,
|
11
11
|
bcc: mail.bcc,
|
12
12
|
subject: mail.subject,
|
13
|
-
|
13
|
+
text_part: extract_mail_body_by_type(mail, "text/plain"),
|
14
|
+
html_part: extract_mail_body_by_type(mail, "text/html"),
|
14
15
|
content_type: mail.content_type,
|
15
16
|
attachments: mail.attachments.map { |a| { filename: a.filename, content_type: a.content_type.split(";").first } }
|
16
17
|
)
|
@@ -18,5 +19,16 @@ module RailsMail
|
|
18
19
|
Rails.logger.error("Failed to deliver email: #{e.message}")
|
19
20
|
raise e
|
20
21
|
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def extract_mail_body_by_type(mail, mime_type)
|
26
|
+
if mail.multipart?
|
27
|
+
part = mail.parts.find { |p| p.content_type.start_with?(mime_type) }
|
28
|
+
part ? part.body.as_json : nil
|
29
|
+
elsif mail.content_type.start_with?(mime_type)
|
30
|
+
mail.body.as_json
|
31
|
+
end
|
32
|
+
end
|
21
33
|
end
|
22
34
|
end
|
data/lib/rails_mail/version.rb
CHANGED
data/lib/rails_mail.rb
CHANGED
@@ -17,24 +17,12 @@ module RailsMail
|
|
17
17
|
@configuration = Configuration.new
|
18
18
|
end
|
19
19
|
|
20
|
-
def authentication_callback
|
21
|
-
|
22
|
-
@authentication_callback || ->(request) { true }
|
20
|
+
def authentication_callback
|
21
|
+
configuration.authentication_callback || ->(request) { true }
|
23
22
|
end
|
24
23
|
|
25
|
-
def
|
26
|
-
|
27
|
-
@show_clear_button || ->(request) { true }
|
24
|
+
def show_clear_all_button_callback
|
25
|
+
configuration.show_clear_all_button || ->(request) { true }
|
28
26
|
end
|
29
27
|
end
|
30
|
-
|
31
|
-
# class Configuration
|
32
|
-
# def authentication_callback(&block)
|
33
|
-
# RailsMail.authentication_callback(&block)
|
34
|
-
# end
|
35
|
-
|
36
|
-
# def show_clear_button(&block)
|
37
|
-
# RailsMail.show_clear_button?(&block)
|
38
|
-
# end
|
39
|
-
# end
|
40
28
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_mail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Philips
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -69,6 +69,7 @@ files:
|
|
69
69
|
- app/frontend/rails_mail/modules/consumer.js
|
70
70
|
- app/frontend/rails_mail/modules/email_highlight_controller.js
|
71
71
|
- app/frontend/rails_mail/modules/emails_channel.js
|
72
|
+
- app/frontend/rails_mail/modules/redirect_controller.js
|
72
73
|
- app/frontend/rails_mail/modules/timeago.js
|
73
74
|
- app/frontend/rails_mail/style.css
|
74
75
|
- app/frontend/rails_mail/vendor/action_cable.js
|
@@ -80,11 +81,14 @@ files:
|
|
80
81
|
- app/helpers/rails_mail/turbo_helper.rb
|
81
82
|
- app/jobs/rails_mail/trim_emails_job.rb
|
82
83
|
- app/models/rails_mail/email.rb
|
84
|
+
- app/views/layouts/rails_mail/_head.html.erb
|
85
|
+
- app/views/layouts/rails_mail/_title.html.erb
|
83
86
|
- app/views/layouts/rails_mail/application.html.erb
|
84
87
|
- app/views/rails_mail/emails/_email.html.erb
|
88
|
+
- app/views/rails_mail/emails/_email_tabs.html.erb
|
85
89
|
- app/views/rails_mail/emails/_empty_state.html.erb
|
86
90
|
- app/views/rails_mail/emails/_form.html.erb
|
87
|
-
- app/views/rails_mail/emails/
|
91
|
+
- app/views/rails_mail/emails/destroy.turbo_stream.erb
|
88
92
|
- app/views/rails_mail/emails/destroy_all.turbo_stream.erb
|
89
93
|
- app/views/rails_mail/emails/edit.html.erb
|
90
94
|
- app/views/rails_mail/emails/index.html.erb
|
@@ -126,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
130
|
- !ruby/object:Gem::Version
|
127
131
|
version: '0'
|
128
132
|
requirements: []
|
129
|
-
rubygems_version: 3.5.
|
133
|
+
rubygems_version: 3.5.22
|
130
134
|
signing_key:
|
131
135
|
specification_version: 4
|
132
136
|
summary: Database-backed Action Mailer delivery method with web interface
|
@@ -1,50 +0,0 @@
|
|
1
|
-
<%= rails_mail_turbo_frame_tag "email_content" do %>
|
2
|
-
<div class="bg-white shadow rounded-lg" data-email-id="<%= email.id %>"
|
3
|
-
data-email-highlight-current-id-value="<%= email.id %>">
|
4
|
-
<div class="px-6 py-4 border-b border-gray-200">
|
5
|
-
<h1 class="text-2xl font-semibold text-gray-900"><%= email.subject %></h1>
|
6
|
-
<div class="mt-2 text-sm text-gray-600">
|
7
|
-
<div>From: <%= email.from %></div>
|
8
|
-
<div>To: <%= email.to.join(", ") %></div>
|
9
|
-
<% if email.cc.present? %>
|
10
|
-
<div>CC: <%= email.cc.join(", ") %></div>
|
11
|
-
<% end %>
|
12
|
-
<% if email.bcc.present? %>
|
13
|
-
<div>BCC: <%= email.bcc.join(", ") %></div>
|
14
|
-
<% end %>
|
15
|
-
<div class="text-gray-400 mt-1">
|
16
|
-
<%= email.created_at.strftime("%B %d, %Y at %I:%M %p") %>
|
17
|
-
</div>
|
18
|
-
</div>
|
19
|
-
</div>
|
20
|
-
|
21
|
-
<div class="px-6 py-4">
|
22
|
-
<% if email.attachments.present? %>
|
23
|
-
<div class="mb-4 p-4 bg-gray-50 rounded-md">
|
24
|
-
<h3 class="text-sm font-medium text-gray-900 mb-2">Attachments</h3>
|
25
|
-
<div class="space-y-2">
|
26
|
-
<% email.attachments.each do |attachment| %>
|
27
|
-
<div class="flex items-center text-sm">
|
28
|
-
<svg class="h-5 w-5 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
29
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
30
|
-
</svg>
|
31
|
-
<span><%= attachment["filename"] %></span>
|
32
|
-
<span class="ml-2 text-gray-500">(<%= attachment["content_type"] %>)</span>
|
33
|
-
</div>
|
34
|
-
<% end %>
|
35
|
-
</div>
|
36
|
-
</div>
|
37
|
-
<% end %>
|
38
|
-
|
39
|
-
<div class="prose max-w-none">
|
40
|
-
<% if email.html? %>
|
41
|
-
<%= sanitize(email.body,
|
42
|
-
tags: ActionView::Base.sanitized_allowed_tags + ['table', 'tbody', 'tr', 'td'],
|
43
|
-
attributes: ActionView::Base.sanitized_allowed_attributes + ['style']) %>
|
44
|
-
<% else %>
|
45
|
-
<%= simple_format email.body %>
|
46
|
-
<% end %>
|
47
|
-
</div>
|
48
|
-
</div>
|
49
|
-
</div>
|
50
|
-
<% end %>
|