sunabamail 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/.github/workflows/main.yml +45 -0
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +119 -0
- data/Rakefile +12 -0
- data/app/controllers/sunabamail/application_controller.rb +4 -0
- data/app/controllers/sunabamail/messages/alls_controller.rb +6 -0
- data/app/controllers/sunabamail/messages/application_controller.rb +9 -0
- data/app/controllers/sunabamail/messages/attachments_controller.rb +13 -0
- data/app/controllers/sunabamail/messages/htmls_controller.rb +12 -0
- data/app/controllers/sunabamail/messages/raws_controller.rb +7 -0
- data/app/controllers/sunabamail/messages/sources_controller.rb +7 -0
- data/app/controllers/sunabamail/messages/texts_controller.rb +7 -0
- data/app/controllers/sunabamail/messages_controller.rb +22 -0
- data/app/models/sunabamail/message.rb +12 -0
- data/app/models/sunabamail/message_raw.rb +6 -0
- data/app/models/sunabamail/record.rb +5 -0
- data/app/views/layouts/sunabamail/application.html.erb +21 -0
- data/app/views/sunabamail/application/_scripts.html.erb +155 -0
- data/app/views/sunabamail/application/_styles.html.erb +281 -0
- data/app/views/sunabamail/messages/_message.html.erb +82 -0
- data/app/views/sunabamail/messages/index.html.erb +71 -0
- data/app/views/sunabamail/messages/show.html.erb +3 -0
- data/assets/sunabamail.png +0 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +16 -0
- data/lib/generators/sunabamail/install_generator.rb +87 -0
- data/lib/generators/sunabamail/templates/db/sunabamail_schema.rb +23 -0
- data/lib/sunabamail/delivery_method.rb +19 -0
- data/lib/sunabamail/engine.rb +27 -0
- data/lib/sunabamail/tasks.rb +6 -0
- data/lib/sunabamail/version.rb +5 -0
- data/lib/sunabamail.rb +26 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 856ebc548796776b7b192691b54668c2e81c9c2cfceb62b2057ab9358071270c
|
|
4
|
+
data.tar.gz: fa2b1dcb6033d4f6ad9a2af74937d07c03b2b8bdbfb2e91d2a9b11c9dd012d2e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fa0b4dc118595c7eb59f2d368c719925ba0e90cdb4bc6b661d9d46083306fcb706c1e8396dbabc9cd4fa22507e12cd7fab8df76eff91bd9ba829b0f1610c37dd
|
|
7
|
+
data.tar.gz: ec79d435cba4ddfaf3bc7dae08b8abc5a2bf8ed2655172242fe7406f495baf387f1bd5cb35ac786b49bc4a2ed6f0ac1fc9be6459bbbe69391aa9a4ed398c05fe
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
name: "Ruby ${{ matrix.ruby }} x Rails ${{ matrix.rails }}"
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
ruby: ['3.1', '3.2', '3.3', '3.4', '4.0']
|
|
17
|
+
rails:
|
|
18
|
+
- '~> 7.0.0'
|
|
19
|
+
- '~> 7.1.0'
|
|
20
|
+
- '~> 7.2.0'
|
|
21
|
+
- '~> 8.0.0'
|
|
22
|
+
- '~> 8.1.0'
|
|
23
|
+
- 'head'
|
|
24
|
+
exclude:
|
|
25
|
+
- ruby: '3.1'
|
|
26
|
+
rails: 'head'
|
|
27
|
+
- ruby: '3.2'
|
|
28
|
+
rails: 'head'
|
|
29
|
+
- ruby: '3.1'
|
|
30
|
+
rails: '~> 8.0.0'
|
|
31
|
+
- ruby: '3.1'
|
|
32
|
+
rails: '~> 8.1.0'
|
|
33
|
+
env:
|
|
34
|
+
RAILS: ${{ matrix.rails }}
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v5
|
|
37
|
+
- name: Set up Ruby
|
|
38
|
+
uses: ruby/setup-ruby@v1
|
|
39
|
+
with:
|
|
40
|
+
ruby-version: ${{ matrix.ruby }}
|
|
41
|
+
bundler-cache: true
|
|
42
|
+
- name: Run test
|
|
43
|
+
run: bundle exec rake test
|
|
44
|
+
- name: Run rubocop
|
|
45
|
+
run: bundle exec rake rubocop
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Takashi SAKAGUCHI
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Sunabamail
|
|
2
|
+
|
|
3
|
+
Sunabamail is an Action Mailer delivery method that stores emails in the database - so you can inspect them from anywhere via a built-in web UI.
|
|
4
|
+
|
|
5
|
+
## Why Sunabamail?
|
|
6
|
+
|
|
7
|
+
In modern Rails applications, emails are often generated across multiple processes — web servers, background workers, and staging environments.
|
|
8
|
+
|
|
9
|
+
Sunabamail is designed for this kind of setup.
|
|
10
|
+
In other words, it makes emails observable regardless of where they are generated.
|
|
11
|
+
|
|
12
|
+
By storing emails in the database, it allows you to:
|
|
13
|
+
|
|
14
|
+
- Access emails from any process or server
|
|
15
|
+
- Inspect emails generated by background jobs
|
|
16
|
+
- Verify email behavior safely in staging
|
|
17
|
+
|
|
18
|
+
It works well alongside traditional development tools, while focusing on setups where emails need to be observable across processes and environments.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- Drop-in replacement for `ActionMailer` delivery method
|
|
23
|
+
- Stores emails in a dedicated database (separate from your app data)
|
|
24
|
+
- Built-in Rails Engine UI (`/sunabamail`) for browsing emails
|
|
25
|
+
- Works across processes and servers
|
|
26
|
+
- Ideal for development and staging
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
bundle add sunabamail --group development
|
|
32
|
+
bin/rails sunabamail:install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This will:
|
|
36
|
+
|
|
37
|
+
- Configure action_mailer.delivery_method
|
|
38
|
+
- Mount the engine
|
|
39
|
+
- Generate db/sunabamail_schema.rb
|
|
40
|
+
|
|
41
|
+
### Database configuration
|
|
42
|
+
|
|
43
|
+
Add a dedicated database for Sunabamail.
|
|
44
|
+
|
|
45
|
+
#### SQLite
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
development:
|
|
49
|
+
primary:
|
|
50
|
+
<<: *default
|
|
51
|
+
database: storage/development.sqlite3
|
|
52
|
+
sunabamail:
|
|
53
|
+
<<: *default
|
|
54
|
+
database: storage/development_sunabamail.sqlite3
|
|
55
|
+
migrations_paths: db/sunabamail_migrate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### MySQL / PostgreSQL / Trilogy
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
development:
|
|
62
|
+
primary: &primary_development
|
|
63
|
+
<<: *default
|
|
64
|
+
database: app_development
|
|
65
|
+
username: app
|
|
66
|
+
password: <%= ENV["APP_DATABASE_PASSWORD"] %>
|
|
67
|
+
sunabamail:
|
|
68
|
+
<<: *primary_development
|
|
69
|
+
database: app_development_sunabamail
|
|
70
|
+
migrations_paths: db/sunabamail_migrate
|
|
71
|
+
```
|
|
72
|
+
Then run:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
bin/rails db:prepare
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Usage
|
|
79
|
+
|
|
80
|
+
Start your Rails server and visit:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
http://localhost:3000/sunabamail
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You will see a list of captured emails and their contents:
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
You can:
|
|
91
|
+
|
|
92
|
+
- Browse captured emails
|
|
93
|
+
- Inspect subject, body, and headers
|
|
94
|
+
- Debug mailer behavior without sending real emails
|
|
95
|
+
|
|
96
|
+
### Usage in staging
|
|
97
|
+
|
|
98
|
+
Sunabamail can also be used in staging environments.
|
|
99
|
+
|
|
100
|
+
This is especially useful when:
|
|
101
|
+
|
|
102
|
+
- You want to verify email behavior without sending real emails
|
|
103
|
+
- Your app runs background jobs on separate workers
|
|
104
|
+
|
|
105
|
+
**NOTE** Not intended for production use.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
110
|
+
|
|
111
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hamajyotan/sunabamail.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Sunabamail::Messages::AttachmentsController < Sunabamail::Messages::ApplicationController
|
|
2
|
+
before_action :set_attachment, only: %i[show]
|
|
3
|
+
|
|
4
|
+
def show
|
|
5
|
+
send_data(@attachment.read, filename: @attachment.filename)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def set_attachment
|
|
11
|
+
@attachment = @message.mail.attachments[params.expect(:id).to_i]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class Sunabamail::Messages::HtmlsController < Sunabamail::Messages::ApplicationController
|
|
2
|
+
def show
|
|
3
|
+
html = @message.mail.html_part.body.to_s.force_encoding(@message.mail.charset)
|
|
4
|
+
doc = Nokogiri::HTML.fragment(html)
|
|
5
|
+
doc.css("a").each do |a|
|
|
6
|
+
a["target"] = "_blank"
|
|
7
|
+
a["rel"] = "noopener noreferrer"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
send_data doc.to_html, type: "text/html", disposition: "inline"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Sunabamail::MessagesController < Sunabamail::ApplicationController
|
|
2
|
+
before_action :set_message, only: %i[show destroy]
|
|
3
|
+
|
|
4
|
+
def index
|
|
5
|
+
@messages = Sunabamail::Message.all.order(id: :desc)
|
|
6
|
+
@message = @messages.first
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def destroy
|
|
13
|
+
@message.destroy!
|
|
14
|
+
redirect_to messages_path, notice: "destroyed", status: :see_other
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def set_message
|
|
20
|
+
@message = Sunabamail::Message.find(params.expect(:id))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class Sunabamail::Message < Sunabamail::Record
|
|
2
|
+
has_one :raw,
|
|
3
|
+
foreign_key: :sunabamail_message_id,
|
|
4
|
+
class_name: "Sunabamail::MessageRaw",
|
|
5
|
+
inverse_of: :message,
|
|
6
|
+
dependent: :destroy,
|
|
7
|
+
autosave: true
|
|
8
|
+
|
|
9
|
+
def mail
|
|
10
|
+
Mail::Message.new(raw.encoded)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= "Sunabamail" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="application-name" content="Sunabamail">
|
|
8
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
9
|
+
<%= csrf_meta_tags %>
|
|
10
|
+
<%= csp_meta_tag %>
|
|
11
|
+
|
|
12
|
+
<%= yield :head %>
|
|
13
|
+
|
|
14
|
+
<%= render "styles" %>
|
|
15
|
+
<%= render "scripts" %>
|
|
16
|
+
</head>
|
|
17
|
+
|
|
18
|
+
<body>
|
|
19
|
+
<%= yield %>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3
|
+
initConfirm();
|
|
4
|
+
initPaneSeparator();
|
|
5
|
+
initMessageList();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
function initConfirm() {
|
|
9
|
+
document.addEventListener('click', function (e) {
|
|
10
|
+
const el = e.target.closest("[data-confirm]") || e.target.closest("[data-turbo-confirm]");
|
|
11
|
+
if (!el) return;
|
|
12
|
+
|
|
13
|
+
const message = el.dataset.confirm || el.dataset.turboConfirm;
|
|
14
|
+
if (!confirm(message)) {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function initPaneSeparator() {
|
|
21
|
+
const separator = document.querySelector('[data-pane-separator]');
|
|
22
|
+
const container = document.querySelector('[data-messages-list-container]');
|
|
23
|
+
const listPane = document.querySelector('[data-messages-list-pane]');
|
|
24
|
+
if (!separator || !container || !listPane) return;
|
|
25
|
+
|
|
26
|
+
let startX = 0;
|
|
27
|
+
let startWidth = 0;
|
|
28
|
+
|
|
29
|
+
separator.addEventListener('pointerdown', (e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
|
|
32
|
+
startX = e.clientX;
|
|
33
|
+
startWidth = listPane.offsetWidth;
|
|
34
|
+
|
|
35
|
+
separator.classList.add('dragging');
|
|
36
|
+
document.body.style.userSelect = 'none';
|
|
37
|
+
|
|
38
|
+
const onMove = (e) => {
|
|
39
|
+
const containerWidth = container.offsetWidth;
|
|
40
|
+
const minWidth = 200;
|
|
41
|
+
const maxWidth = Math.max(minWidth, containerWidth - 250);
|
|
42
|
+
|
|
43
|
+
const diffX = e.clientX - startX;
|
|
44
|
+
const newWidth = startWidth + diffX;
|
|
45
|
+
|
|
46
|
+
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
|
47
|
+
listPane.style.flex = `0 0 ${newWidth}px`;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onUp = () => {
|
|
52
|
+
separator.classList.remove('dragging');
|
|
53
|
+
document.body.style.userSelect = '';
|
|
54
|
+
|
|
55
|
+
document.removeEventListener('pointermove', onMove);
|
|
56
|
+
document.removeEventListener('pointerup', onUp);
|
|
57
|
+
window.removeEventListener('blur', onUp);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
document.addEventListener('pointermove', onMove);
|
|
61
|
+
document.addEventListener('pointerup', onUp);
|
|
62
|
+
window.addEventListener('blur', onUp);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function initMessageList() {
|
|
67
|
+
const mailListContainer = document.querySelector('[data-mail-list-container]');
|
|
68
|
+
const mailContainer = document.querySelector('[data-mail-container]');
|
|
69
|
+
if (!mailListContainer || !mailContainer) return;
|
|
70
|
+
|
|
71
|
+
const mailListItems = mailListContainer.querySelectorAll('[data-mail-list-item]');
|
|
72
|
+
if (!mailListItems.length) return;
|
|
73
|
+
|
|
74
|
+
const mailErrorMessage = mailListContainer.querySelector('[data-mail-error-message]');
|
|
75
|
+
|
|
76
|
+
let currentRequestId = 0;
|
|
77
|
+
|
|
78
|
+
mailListContainer.addEventListener('click', (e) => {
|
|
79
|
+
const item = e.target.closest('[data-mail-list-item]');
|
|
80
|
+
if (!item) return;
|
|
81
|
+
if (e.target.closest('[data-mail-list-item-delete]')) return;
|
|
82
|
+
|
|
83
|
+
const messagePath = item.dataset.messagePath;
|
|
84
|
+
if (!messagePath) return;
|
|
85
|
+
|
|
86
|
+
mailListItems.forEach(i => i.classList.remove('active'));
|
|
87
|
+
item.classList.add('active');
|
|
88
|
+
|
|
89
|
+
fetchMessageDetail(messagePath);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
mailContainer.addEventListener('click', (e) => {
|
|
93
|
+
const handle = e.target.closest('[data-mail-view-handle]');
|
|
94
|
+
if (!handle) return;
|
|
95
|
+
|
|
96
|
+
activateMailView(handle);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const firstMailListItem = mailListItems[0];
|
|
100
|
+
if (firstMailListItem) {
|
|
101
|
+
firstMailListItem.classList.add('active');
|
|
102
|
+
fetchMessageDetail(firstMailListItem.dataset.messagePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fetchMessageDetail(url) {
|
|
106
|
+
const requestId = ++currentRequestId;
|
|
107
|
+
|
|
108
|
+
fetch(url, {
|
|
109
|
+
method: 'GET',
|
|
110
|
+
headers: {
|
|
111
|
+
'Accept': 'text/html'
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.then(response => {
|
|
115
|
+
if (!response.ok) throw new Error('Failed to load message');
|
|
116
|
+
return response.text();
|
|
117
|
+
})
|
|
118
|
+
.then(html => {
|
|
119
|
+
if (requestId !== currentRequestId) return;
|
|
120
|
+
|
|
121
|
+
// Extract the main content div from the response
|
|
122
|
+
const parser = new DOMParser();
|
|
123
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
124
|
+
const messageContent = doc.querySelector('[data-mail-detail-root]')
|
|
125
|
+
|
|
126
|
+
if (messageContent) {
|
|
127
|
+
mailContainer.innerHTML = messageContent.innerHTML;
|
|
128
|
+
const firstHandle = mailContainer.querySelector('[data-mail-view-handle]')
|
|
129
|
+
firstHandle && activateMailView(firstHandle);
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.catch(error => {
|
|
133
|
+
console.error('Error loading message:', error);
|
|
134
|
+
if (mailErrorMessage) {
|
|
135
|
+
mailContainer.innerHTML = mailErrorMessage.innerHTML;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function activateMailView(handle) {
|
|
141
|
+
mailContainer.querySelectorAll('[data-mail-view-handle]').forEach(i => i.classList.remove('active'));
|
|
142
|
+
handle.classList.add('active');
|
|
143
|
+
mailContainer.querySelectorAll('[data-mail-view]').forEach((v) => {
|
|
144
|
+
const active = (v.dataset.mailView === handle.dataset.mailViewTarget);
|
|
145
|
+
v.classList.toggle('active', active);
|
|
146
|
+
if (active) {
|
|
147
|
+
const iframe = v.querySelector('iframe');
|
|
148
|
+
if (iframe && !iframe.src && iframe.dataset.src) {
|
|
149
|
+
iframe.src = iframe.dataset.src;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.message-header-container {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.message-header {
|
|
8
|
+
display: grid;
|
|
9
|
+
grid-template-columns: auto 1fr;
|
|
10
|
+
gap: 0 1rem;
|
|
11
|
+
border: 1px solid #dee2e6;
|
|
12
|
+
border-radius: 0.25rem;
|
|
13
|
+
padding: 1rem;
|
|
14
|
+
background-color: #f8f9fa;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.message-header dl {
|
|
18
|
+
margin: 0;
|
|
19
|
+
display: contents;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.message-header small {
|
|
23
|
+
grid-column: 1 / -1;
|
|
24
|
+
margin-top: 0.5rem;
|
|
25
|
+
color: #6c757d;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.message-subject {
|
|
29
|
+
margin-top: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Message Views Tabs */
|
|
33
|
+
.message-views-tabs-container {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.message-views-tabs {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 0.5rem;
|
|
41
|
+
margin: 1rem 0;
|
|
42
|
+
border-bottom: 2px solid #e0e0e0;
|
|
43
|
+
padding-bottom: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.message-view-tab {
|
|
47
|
+
padding: 0.5rem 1rem;
|
|
48
|
+
background: none;
|
|
49
|
+
border: none;
|
|
50
|
+
border-bottom: 3px solid transparent;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
font-size: 0.9rem;
|
|
53
|
+
color: #666;
|
|
54
|
+
transition: color 0.2s, border-color 0.2s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.message-view-tab:hover {
|
|
58
|
+
color: #333;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.message-view-tab.active {
|
|
62
|
+
color: #0066cc;
|
|
63
|
+
border-bottom-color: #0066cc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.message-view-item {
|
|
67
|
+
display: none;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
flex: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.message-view-item.active {
|
|
73
|
+
display: flex;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.message-view-item iframe {
|
|
77
|
+
width: 100%;
|
|
78
|
+
min-height: 400px;
|
|
79
|
+
border: 1px solid #e0e0e0;
|
|
80
|
+
border-radius: 0.25rem;
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
flex: 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.message-attachments {
|
|
87
|
+
padding: 1rem .25rem 0;
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.message-attachments a {
|
|
92
|
+
display: inline-block;
|
|
93
|
+
margin-right: 1rem;
|
|
94
|
+
margin-bottom: 0.5rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.message-header dt {
|
|
98
|
+
color: #495057;
|
|
99
|
+
grid-column: 1;
|
|
100
|
+
font-size: 0.875rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.message-header dd {
|
|
104
|
+
margin: 0;
|
|
105
|
+
color: #212529;
|
|
106
|
+
grid-column: 2;
|
|
107
|
+
word-break: break-word;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.message-item-header {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: auto 1fr;
|
|
113
|
+
gap: 0 0.5rem;
|
|
114
|
+
margin: 0.25rem 0;
|
|
115
|
+
font-size: 0.875rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.message-item-header dt {
|
|
119
|
+
display: inline;
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.message-item-header dd {
|
|
124
|
+
display: inline;
|
|
125
|
+
margin: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.message-item-header small {
|
|
129
|
+
font-size: 0.75rem;
|
|
130
|
+
color: #999;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.messages-toolbar {
|
|
134
|
+
display: flex;
|
|
135
|
+
gap: 0.5rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.messages-toolbar button,
|
|
139
|
+
.messages-toolbar form {
|
|
140
|
+
display: inline-block;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.messages-toolbar button img, .messages-toolbar button svg {
|
|
144
|
+
width: 20px;
|
|
145
|
+
height: 20px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.messages-container {
|
|
149
|
+
display: flex;
|
|
150
|
+
gap: 0;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.pane-separator {
|
|
155
|
+
flex: 0 0 8px;
|
|
156
|
+
cursor: col-resize;
|
|
157
|
+
user-select: none;
|
|
158
|
+
transition: background-color 0.2s;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.pane-separator:hover {
|
|
162
|
+
background-color: #a0a0a0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.pane-separator.dragging {
|
|
166
|
+
background-color: #a0a0a0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.messages-list-pane {
|
|
170
|
+
flex: 0 0 30%;
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
height: calc(100vh - 2rem);
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
min-height: 0;
|
|
176
|
+
background-color: #fafafa;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.messages-list-pane header {
|
|
180
|
+
margin: 1rem 1rem .5rem;
|
|
181
|
+
position: sticky;
|
|
182
|
+
top: 0;
|
|
183
|
+
z-index: 1;
|
|
184
|
+
background-color: #fafafa;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.messages-list-pane header h1 {
|
|
188
|
+
margin: 0 0 .5rem;
|
|
189
|
+
font-size: 1.25rem;
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.messages-list-pane header h1 a {
|
|
194
|
+
color: inherit;
|
|
195
|
+
text-decoration: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.messages-list-pane header h1 a:hover,
|
|
199
|
+
.messages-list-pane header h1 a:focus {
|
|
200
|
+
text-decoration: none;
|
|
201
|
+
color: #333;
|
|
202
|
+
opacity: 0.8;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.messages-list-pane ul {
|
|
206
|
+
list-style: none;
|
|
207
|
+
padding: 0;
|
|
208
|
+
margin: 0;
|
|
209
|
+
overflow-y: auto;
|
|
210
|
+
flex: 1;
|
|
211
|
+
min-height: 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.messages-list-pane li {
|
|
215
|
+
border-bottom: 1px solid #e0e0e0;
|
|
216
|
+
padding: 1rem;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
display: flex;
|
|
219
|
+
justify-content: space-between;
|
|
220
|
+
align-items: flex-start;
|
|
221
|
+
gap: 0.5rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.messages-list-pane li:hover {
|
|
225
|
+
background-color: #f0f0f0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.messages-list-pane li.active {
|
|
229
|
+
background-color: #e3f2fd;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.message-item-content {
|
|
233
|
+
flex: 1;
|
|
234
|
+
min-width: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.message-item-content p {
|
|
238
|
+
margin: 0 0 0.5rem 0;
|
|
239
|
+
font-weight: 500;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
text-overflow: ellipsis;
|
|
242
|
+
white-space: nowrap;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.message-item-content dl {
|
|
246
|
+
margin: 0.25rem 0;
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.message-item-content dt,
|
|
251
|
+
.message-item-content dd {
|
|
252
|
+
display: inline;
|
|
253
|
+
margin: 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.message-item-content dt::after {
|
|
257
|
+
content: " ";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.message-item-content dd::after {
|
|
261
|
+
content: "\A";
|
|
262
|
+
white-space: pre;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.message-item-content span {
|
|
266
|
+
font-size: 0.75rem;
|
|
267
|
+
color: #999;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.message-item-delete-button {
|
|
271
|
+
font-size: 1rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.messages-detail-pane {
|
|
275
|
+
display: flex;
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
flex: 1;
|
|
278
|
+
padding: 1rem;
|
|
279
|
+
background-color: white;
|
|
280
|
+
}
|
|
281
|
+
</style>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<h2 class="message-subject"><%= object.subject %></h2>
|
|
2
|
+
|
|
3
|
+
<div class="message-header-container">
|
|
4
|
+
<div class="message-header">
|
|
5
|
+
<dl>
|
|
6
|
+
<% if object.from %>
|
|
7
|
+
<dt>From:</dt>
|
|
8
|
+
<dd><%= object.from %></dd>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% if object.sender %>
|
|
12
|
+
<dt>Sender:</dt>
|
|
13
|
+
<dd><%= object.sender %></dd>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% if object.to %>
|
|
17
|
+
<dt>To:</dt>
|
|
18
|
+
<dd><%= object.to %></dd>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if object.cc %>
|
|
22
|
+
<dt>Cc:</dt>
|
|
23
|
+
<dd><%= object.cc %></dd>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if object.bcc %>
|
|
27
|
+
<dt>Bcc:</dt>
|
|
28
|
+
<dd><%= object.bcc %></dd>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<% if object.reply_to %>
|
|
32
|
+
<dt>Reply-To:</dt>
|
|
33
|
+
<dd><%= object.reply_to %></dd>
|
|
34
|
+
<% end %>
|
|
35
|
+
</dl>
|
|
36
|
+
|
|
37
|
+
<small><%= l object.created_at, format: :long %></small>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<% mail = object.mail %>
|
|
42
|
+
|
|
43
|
+
<% if mail.attachments.any? %>
|
|
44
|
+
<div class="message-attachments">
|
|
45
|
+
<% mail.attachments.each.with_index do |attachment, index| %>
|
|
46
|
+
<%= link_to attachment.filename, message_attachment_path(object, index), download: true %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<% has_html = mail.html_part.present? %>
|
|
52
|
+
<% has_text = mail.text_part.present? %>
|
|
53
|
+
|
|
54
|
+
<div class="message-views-tabs-container">
|
|
55
|
+
<div class="message-views-tabs">
|
|
56
|
+
<% if has_html %>
|
|
57
|
+
<button class="message-view-tab" data-mail-view-handle data-mail-view-target="html">Html</button>
|
|
58
|
+
<button class="message-view-tab" data-mail-view-handle data-mail-view-target="source">Html Source</button>
|
|
59
|
+
<% end %>
|
|
60
|
+
<% if has_text %>
|
|
61
|
+
<button class="message-view-tab" data-mail-view-handle data-mail-view-target="text">Text</button>
|
|
62
|
+
<% end %>
|
|
63
|
+
<button class="message-view-tab" data-mail-view-handle data-mail-view-target="raw">Raw</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<% if has_html %>
|
|
68
|
+
<div class="message-view-item" data-mail-view="html">
|
|
69
|
+
<%= tag.iframe sealmess: "sealmess", data: { src: message_html_path(object) } %>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="message-view-item" data-mail-view="source">
|
|
72
|
+
<%= tag.iframe sealmess: "sealmess", data: { src: message_source_path(object) } %>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
75
|
+
<% if has_text %>
|
|
76
|
+
<div class="message-view-item" data-mail-view="text">
|
|
77
|
+
<%= tag.iframe sealmess: "sealmess", data: { src: message_text_path(object) } %>
|
|
78
|
+
</div>
|
|
79
|
+
<% end %>
|
|
80
|
+
<div class="message-view-item" data-mail-view="raw">
|
|
81
|
+
<%= tag.iframe sealmess: "sealmess", data: { src: message_raw_path(object) } %>
|
|
82
|
+
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template data-mail-error-message>
|
|
2
|
+
<%= t("sunabamail.failed_to_load_email") %>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<div class="messages-container" data-messages-list-container>
|
|
6
|
+
<!-- Left Pane: Messages List -->
|
|
7
|
+
<div class="messages-list-pane" data-messages-list-pane>
|
|
8
|
+
<header>
|
|
9
|
+
<h1><%= link_to "Sunabamail", root_path %></h1>
|
|
10
|
+
<div class="messages-toolbar">
|
|
11
|
+
<%= button_to messages_path, method: :get, form: { style: "display: inline;" } do %>
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
13
|
+
<path d="M4 12a8 8 0 0 1 13.66-5.66"/>
|
|
14
|
+
<polyline points="18 3 18 8 13 8"/>
|
|
15
|
+
<path d="M20 12a8 8 0 0 1-13.66 5.66"/>
|
|
16
|
+
<polyline points="6 21 6 16 11 16"/>
|
|
17
|
+
</svg>
|
|
18
|
+
<% end %>
|
|
19
|
+
<%= button_to messages_all_path, method: :delete,
|
|
20
|
+
disabled: @messages.blank?,
|
|
21
|
+
form: { style: "display: inline;" },
|
|
22
|
+
data: { turbo_confirm: t("sunabamail.destroy_confirm") } do %>
|
|
23
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
24
|
+
<path d="M3 6h18"/>
|
|
25
|
+
<path d="M8 6V4h8v2"/>
|
|
26
|
+
<path d="M6 6l1 14h10l1-14"/>
|
|
27
|
+
<path d="M10 11v6"/>
|
|
28
|
+
<path d="M14 11v6"/>
|
|
29
|
+
</svg>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<ul id="messages" data-mail-list-container>
|
|
35
|
+
<p style="color: green"><%= notice %></p>
|
|
36
|
+
|
|
37
|
+
<% @messages.each do |message| %>
|
|
38
|
+
<li class="message-item" data-mail-list-item data-message-path="<%= message_path(message) %>">
|
|
39
|
+
<div class="message-item-content">
|
|
40
|
+
<p><%= message.subject %></p>
|
|
41
|
+
<dl class="message-item-header">
|
|
42
|
+
<% if message.from %>
|
|
43
|
+
<dt>From:</dt>
|
|
44
|
+
<dd><%= message.from %></dd>
|
|
45
|
+
<% end %>
|
|
46
|
+
<% if message.to %>
|
|
47
|
+
<dt>To:</dt>
|
|
48
|
+
<dd><%= message.to %></dd>
|
|
49
|
+
<% end %>
|
|
50
|
+
</dl>
|
|
51
|
+
<span><%= time_ago_in_words message.created_at %></span>
|
|
52
|
+
</div>
|
|
53
|
+
<%= button_to message_path(message), method: :delete,
|
|
54
|
+
class: "message-item-delete-button",
|
|
55
|
+
data: { turbo_confirm: t("sunabamail.destroy_confirm") },
|
|
56
|
+
form: { data: { mail_list_item_delete: true } } do %>
|
|
57
|
+
×
|
|
58
|
+
<% end %>
|
|
59
|
+
</li>
|
|
60
|
+
<% end %>
|
|
61
|
+
</ul>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Resizable Separator -->
|
|
65
|
+
<div class="pane-separator" data-pane-separator></div>
|
|
66
|
+
|
|
67
|
+
<!-- Right Pane: Message Detail -->
|
|
68
|
+
<div class="messages-detail-pane" data-mail-container>
|
|
69
|
+
<p><%= t("sunabamail.no_mail_items") %></p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
Binary file
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Sunabamail::Engine.routes.draw do
|
|
2
|
+
namespace :messages do
|
|
3
|
+
resource :all, only: %i[destroy]
|
|
4
|
+
end
|
|
5
|
+
resources :messages, only: %i[index show destroy] do
|
|
6
|
+
scope module: :messages do
|
|
7
|
+
resources :attachments, only: %i[show]
|
|
8
|
+
resource :html, only: %i[show]
|
|
9
|
+
resource :raw, only: %i[show]
|
|
10
|
+
resource :source, only: %i[show]
|
|
11
|
+
resource :text, only: %i[show]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
root to: redirect("messages")
|
|
16
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Sunabamail::InstallGenerator < Rails::Generators::Base
|
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
|
5
|
+
|
|
6
|
+
def copy_files
|
|
7
|
+
template "db/sunabamail_schema.rb"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def configure_adapter
|
|
11
|
+
pathname = Pathname(destination_root).join("config/environments/development.rb")
|
|
12
|
+
|
|
13
|
+
if File.read(pathname).match?(/config\.action_mailer\.delivery_method\s*=/)
|
|
14
|
+
gsub_file pathname,
|
|
15
|
+
/(# )?config\.action_mailer\.delivery_method\s+=.*\n/,
|
|
16
|
+
"config.action_mailer.delivery_method = :sunabamail\n"
|
|
17
|
+
else
|
|
18
|
+
environment <<~RUBY, env: "development"
|
|
19
|
+
config.action_mailer.delivery_method = :sunabamail
|
|
20
|
+
RUBY
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure_database
|
|
25
|
+
environment <<~RUBY, env: "development"
|
|
26
|
+
config.sunabamail.connects_to = { database: { writing: :sunabamail } }
|
|
27
|
+
RUBY
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_mount_routes
|
|
31
|
+
route <<~ROUTE
|
|
32
|
+
if Rails.configuration.action_mailer.delivery_method == :sunabamail
|
|
33
|
+
mount Sunabamail::Engine => "/sunabamail"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
ROUTE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def database_configuration_hint
|
|
40
|
+
say ""
|
|
41
|
+
say "#{set_color('✔', :green)} Sunabamail setup complete"
|
|
42
|
+
say ""
|
|
43
|
+
|
|
44
|
+
say "Changes applied:", :blue
|
|
45
|
+
say
|
|
46
|
+
say " • Updated config/environments/development.rb"
|
|
47
|
+
say " - action_mailer.delivery_method = :sunabamail"
|
|
48
|
+
say " - config.sunabamail.connects_to = { database: { writing: :sunabamail } }"
|
|
49
|
+
say " • Updated config/routes.rb"
|
|
50
|
+
say " - Mounted Sunabamail::Engine at /sunabamail (when delivery_method is :sunabamail)"
|
|
51
|
+
say ""
|
|
52
|
+
|
|
53
|
+
say "Next steps:", :blue
|
|
54
|
+
say
|
|
55
|
+
|
|
56
|
+
say "1. Update config/database.yml:", :bold
|
|
57
|
+
say ""
|
|
58
|
+
say " # if you're SQLite, it'll look like this:"
|
|
59
|
+
say ""
|
|
60
|
+
say " development:"
|
|
61
|
+
say " primary:"
|
|
62
|
+
say " <<: *default"
|
|
63
|
+
say " database: storage/development.sqlite3"
|
|
64
|
+
say " sunabamail:"
|
|
65
|
+
say " <<: *default"
|
|
66
|
+
say " database: storage/development_sunabamail.sqlite3"
|
|
67
|
+
say " migrations_paths: db/sunabamail_migrate"
|
|
68
|
+
say ""
|
|
69
|
+
say " # ...or if you're using MySQL/PostgreSQL/Trilogy:"
|
|
70
|
+
say ""
|
|
71
|
+
say " development:"
|
|
72
|
+
say " primary: &primary_development"
|
|
73
|
+
say " <<: *default"
|
|
74
|
+
say " database: app_development"
|
|
75
|
+
say " username: app"
|
|
76
|
+
say " password: <%= ENV[\"APP_DATABASE_PASSWORD\"] %>"
|
|
77
|
+
say " sunabamail:"
|
|
78
|
+
say " <<: *primary_development"
|
|
79
|
+
say " database: app_development_sunabamail"
|
|
80
|
+
say " migrations_paths: db/sunabamail_migrate"
|
|
81
|
+
say ""
|
|
82
|
+
say "2. Run:"
|
|
83
|
+
say ""
|
|
84
|
+
say " $ rails db:prepare"
|
|
85
|
+
say ""
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
ActiveRecord::Schema[7.1].define(version: 1) do
|
|
2
|
+
create_table "sunabamail_message_raws", id: :bigint, force: :cascade do |t|
|
|
3
|
+
t.datetime "created_at", null: false
|
|
4
|
+
t.text "encoded", null: false
|
|
5
|
+
t.bigint "sunabamail_message_id", null: false
|
|
6
|
+
t.datetime "updated_at", null: false
|
|
7
|
+
t.index ["sunabamail_message_id"], name: "index_sunabamail_message_raws_on_sunabamail_message_id", unique: true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
create_table "sunabamail_messages", id: :bigint, force: :cascade do |t|
|
|
11
|
+
t.string "bcc"
|
|
12
|
+
t.string "cc"
|
|
13
|
+
t.datetime "created_at", null: false
|
|
14
|
+
t.string "from"
|
|
15
|
+
t.string "reply_to"
|
|
16
|
+
t.string "sender"
|
|
17
|
+
t.string "subject"
|
|
18
|
+
t.string "to"
|
|
19
|
+
t.datetime "updated_at", null: false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_foreign_key "sunabamail_message_raws", "sunabamail_messages"
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunabamail
|
|
4
|
+
class DeliveryMethod
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def deliver!(mail)
|
|
9
|
+
subject = mail.subject
|
|
10
|
+
created_at = mail.date || Time.zone.now
|
|
11
|
+
updated_at = created_at
|
|
12
|
+
addresses = %i[bcc cc from reply_to sender to].to_h { [ _1, mail[_1].to_s.presence ] }
|
|
13
|
+
|
|
14
|
+
message = Sunabamail::Message.new(**addresses, subject:, created_at:, updated_at:)
|
|
15
|
+
message.build_raw(encoded: mail.encoded)
|
|
16
|
+
message.save!
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunabamail
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Sunabamail
|
|
6
|
+
|
|
7
|
+
rake_tasks do
|
|
8
|
+
load "sunabamail/tasks.rb"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
config.sunabamail = ActiveSupport::OrderedOptions.new
|
|
12
|
+
|
|
13
|
+
initializer "sunabamail.config" do
|
|
14
|
+
Sunabamail.connects_to = { database: { writing: :sunabamail } }
|
|
15
|
+
|
|
16
|
+
config.sunabamail.each do |name, value|
|
|
17
|
+
Sunabamail.public_send("#{name}=", value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "sunabamail.add_delivery_method" do
|
|
22
|
+
ActiveSupport.on_load :action_mailer do
|
|
23
|
+
ActionMailer::Base.add_delivery_method(:sunabamail, Sunabamail::DeliveryMethod)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/sunabamail.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sunabamail/version"
|
|
4
|
+
|
|
5
|
+
require "zeitwerk"
|
|
6
|
+
|
|
7
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
8
|
+
loader.ignore("#{__dir__}/sunabamail/tasks.rb")
|
|
9
|
+
loader.ignore("#{__dir__}/generators")
|
|
10
|
+
loader.setup
|
|
11
|
+
|
|
12
|
+
module Sunabamail
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
attr_accessor :connects_to
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
require "rails"
|
|
20
|
+
rescue LoadError
|
|
21
|
+
# do nothing.
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if defined?(Rails::Engine)
|
|
25
|
+
require "sunabamail/engine"
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sunabamail
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- hamajyotan
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionmailer
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: railties
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.0'
|
|
54
|
+
description: Provides a custom Action Mailer delivery method that stores outgoing
|
|
55
|
+
emails in the database instead of sending them. Stored emails can be viewed through
|
|
56
|
+
a dedicated interface, making it useful for development and staging environments.
|
|
57
|
+
email:
|
|
58
|
+
- hamajyotan@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".github/workflows/main.yml"
|
|
64
|
+
- ".rubocop.yml"
|
|
65
|
+
- CHANGELOG.md
|
|
66
|
+
- LICENSE.txt
|
|
67
|
+
- README.md
|
|
68
|
+
- Rakefile
|
|
69
|
+
- app/controllers/sunabamail/application_controller.rb
|
|
70
|
+
- app/controllers/sunabamail/messages/alls_controller.rb
|
|
71
|
+
- app/controllers/sunabamail/messages/application_controller.rb
|
|
72
|
+
- app/controllers/sunabamail/messages/attachments_controller.rb
|
|
73
|
+
- app/controllers/sunabamail/messages/htmls_controller.rb
|
|
74
|
+
- app/controllers/sunabamail/messages/raws_controller.rb
|
|
75
|
+
- app/controllers/sunabamail/messages/sources_controller.rb
|
|
76
|
+
- app/controllers/sunabamail/messages/texts_controller.rb
|
|
77
|
+
- app/controllers/sunabamail/messages_controller.rb
|
|
78
|
+
- app/models/sunabamail/message.rb
|
|
79
|
+
- app/models/sunabamail/message_raw.rb
|
|
80
|
+
- app/models/sunabamail/record.rb
|
|
81
|
+
- app/views/layouts/sunabamail/application.html.erb
|
|
82
|
+
- app/views/sunabamail/application/_scripts.html.erb
|
|
83
|
+
- app/views/sunabamail/application/_styles.html.erb
|
|
84
|
+
- app/views/sunabamail/messages/_message.html.erb
|
|
85
|
+
- app/views/sunabamail/messages/index.html.erb
|
|
86
|
+
- app/views/sunabamail/messages/show.html.erb
|
|
87
|
+
- assets/sunabamail.png
|
|
88
|
+
- config/locales/en.yml
|
|
89
|
+
- config/routes.rb
|
|
90
|
+
- lib/generators/sunabamail/install_generator.rb
|
|
91
|
+
- lib/generators/sunabamail/templates/db/sunabamail_schema.rb
|
|
92
|
+
- lib/sunabamail.rb
|
|
93
|
+
- lib/sunabamail/delivery_method.rb
|
|
94
|
+
- lib/sunabamail/engine.rb
|
|
95
|
+
- lib/sunabamail/tasks.rb
|
|
96
|
+
- lib/sunabamail/version.rb
|
|
97
|
+
homepage: https://github.com/hamajyotan/sunabamail
|
|
98
|
+
licenses:
|
|
99
|
+
- MIT
|
|
100
|
+
metadata:
|
|
101
|
+
homepage_uri: https://github.com/hamajyotan/sunabamail
|
|
102
|
+
source_code_uri: https://github.com/hamajyotan/sunabamail
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 3.1.0
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 4.0.3
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: A drop-in Action Mailer delivery method that stores emails in the database
|
|
120
|
+
instead of sending them.
|
|
121
|
+
test_files: []
|