letter_thief 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/README.md +122 -0
- data/Rakefile +8 -0
- data/app/controllers/letter_thief/application_controller.rb +4 -0
- data/app/controllers/letter_thief/email_messages_controller.rb +17 -0
- data/app/helpers/letter_thief/application_helper.rb +16 -0
- data/app/jobs/letter_thief/application_job.rb +4 -0
- data/app/models/letter_thief/application_record.rb +5 -0
- data/app/models/letter_thief/email_message.rb +27 -0
- data/app/models/letter_thief/email_search.rb +50 -0
- data/app/views/layouts/letter_thief/application.html.erb +18 -0
- data/app/views/letter_thief/email_messages/index.html.erb +92 -0
- data/app/views/letter_thief/email_messages/show.html.erb +124 -0
- data/config/routes.rb +3 -0
- data/lib/generators/letter_thief/install_generator.rb +24 -0
- data/lib/generators/letter_thief/templates/create_email_messages.rb.erb +33 -0
- data/lib/letter_thief/delivery_method.rb +16 -0
- data/lib/letter_thief/engine.rb +20 -0
- data/lib/letter_thief/interceptor.rb +32 -0
- data/lib/letter_thief/version.rb +3 -0
- data/lib/letter_thief.rb +6 -0
- data/lib/tasks/letter_thief_tasks.rake +4 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f34879896479445bff76a77165e6f7db08a18880e14df630b76c747f76fc64af
|
4
|
+
data.tar.gz: 6db1a3fde6c19127cb57a939b48e98cbf4183eab230ede479909279edd687923
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5c772c8efb3b683164e883057ed94bea7497643f1aeb32e39f76a8430e25bea178c69a4d79a8178d93c277cca7e7be055b01892ef726649af33952ed79ea1edb
|
7
|
+
data.tar.gz: 05dfe63a3bfbe3ff7bad33e0ec8e5ca3e46728315cd8ceebabde3d59609b37c69876a991e390f4f9109ec5e3406b178f654aab16dff90f2d6b77b4eb41e9cf25
|
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# LetterThief
|
2
|
+
|
3
|
+
<img alt="logo.webp" src="logo.png" width="150px"/>
|
4
|
+
|
5
|
+
**A Rails engine to log email deliveries, visualize them in the Browser and, if you want, intercept them.**
|
6
|
+
|
7
|
+
This gem allows you to log on the database the emails sent by your application.
|
8
|
+
|
9
|
+
It provides an interface to visualize the emails directly in the Browser, and search them.
|
10
|
+
|
11
|
+
You can of course navigate and search your emails using ActiveRecord `LetterThief::EmailMessage` model and integrate it
|
12
|
+
other parts of your app.
|
13
|
+
|
14
|
+
> List of interecepted email
|
15
|
+

|
16
|
+
|
17
|
+
> Email preview
|
18
|
+

|
19
|
+
|
20
|
+
You can also use `Letter Thief` as a delivery method in your application as well.
|
21
|
+
|
22
|
+
Set `config.action_mailer.delivery_method = :letter_thief` to stop sending emails and have them only logged in your
|
23
|
+
database. That's particularly useful in development environments or staging/pre-production environments as well.
|
24
|
+
|
25
|
+
If you used `letter_opener` in the past you know how nice it is to have
|
26
|
+
the sent emails opened automatically in your Browser when working locally.
|
27
|
+
LetterThief supports this as well with the very same mechanism, but `launchy` is not a direct dependency.
|
28
|
+
If you add Launchy on your Gemfile, emails will be opened right away once sent.
|
29
|
+
|
30
|
+
Since emails are persisted on the Database it means you can use this also on Heroku, deplo.io, or other PaaS where you
|
31
|
+
don't have a disk. No need for an external service like Mailtrap (amazing service!) anymore.
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
|
35
|
+
Add this line to your application's Gemfile:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
gem "letter_thief"
|
39
|
+
```
|
40
|
+
|
41
|
+
And then execute:
|
42
|
+
|
43
|
+
```bash
|
44
|
+
bundle install
|
45
|
+
bin/rails generate letter_thief:install
|
46
|
+
bin/rails db:migrate
|
47
|
+
```
|
48
|
+
|
49
|
+
This will create the necessary tables.
|
50
|
+
|
51
|
+
If you want to stop sending emails, you can use it also as delivery method:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
config.action_mailer.delivery_method = :letter_thief
|
55
|
+
config.action_mailer.raise_delivery_errors = true
|
56
|
+
```
|
57
|
+
|
58
|
+
If you want emails to be opened immediately, add `launchy` to your Gemfile.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
group :development do
|
62
|
+
gem "launchy"
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
and LetterThief will open emails directly in your Browser like Letter Opener.
|
67
|
+
|
68
|
+
### Separate database
|
69
|
+
|
70
|
+
If you'd like to store your emails on a separate database, you can configure it via:
|
71
|
+
|
72
|
+
`config.letter_thief.connects_to = {database: {writing: :letter_thief}}` and of course you'll need a separate database
|
73
|
+
configuration. Here is an example:
|
74
|
+
|
75
|
+
```yml
|
76
|
+
development:
|
77
|
+
primary:
|
78
|
+
<<: *default
|
79
|
+
database: my_app_development
|
80
|
+
letter_thief:
|
81
|
+
<<: *default
|
82
|
+
database: my_app_letter_thief_development
|
83
|
+
migrations_paths: db/letter_thief_migrate
|
84
|
+
```
|
85
|
+
|
86
|
+
## Varia
|
87
|
+
|
88
|
+
> [!NOTE]
|
89
|
+
> Persisting the emails on the database might have privacy related concerns. You might want to encrypt stuff.
|
90
|
+
|
91
|
+
> [!NOTE]
|
92
|
+
> You might want to schedule a cleanup job to remove old records from time to time.
|
93
|
+
|
94
|
+
> [!NOTE]
|
95
|
+
> For this gem I was heavily inspired by letter_opener, which I used for over ten years. ❤️
|
96
|
+
|
97
|
+
> [!NOTE]
|
98
|
+
> I planned to basically just replicate letter_opener but persist emails in the database, so I could use it on Heroku or
|
99
|
+
> deplo.io, that's why is called LetterThief. It was supposed to just intercept and block emails from being sent. It
|
100
|
+
> ended up being this and also a logging system.
|
101
|
+
|
102
|
+
## Development
|
103
|
+
|
104
|
+
You can run the tests with `bin/test`. By default they run on sqlite. To run them on postgres or mysql, specify the
|
105
|
+
TARGET_DB.
|
106
|
+
|
107
|
+
```bash
|
108
|
+
bin/test #sqlite
|
109
|
+
TARGET_DB=postgres bin/test #postgres
|
110
|
+
TARGET_DB=mysql bin/test #mysql
|
111
|
+
```
|
112
|
+
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/coorasse/letter_thief.
|
116
|
+
|
117
|
+
Try to be a decent human being while interacting with other people.
|
118
|
+
|
119
|
+
## License
|
120
|
+
|
121
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
122
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LetterThief
|
4
|
+
class EmailMessagesController < ApplicationController
|
5
|
+
layout "letter_thief/application"
|
6
|
+
|
7
|
+
PAGE_SIZE = 20
|
8
|
+
|
9
|
+
def index
|
10
|
+
@search = LetterThief::EmailSearch.new(params).perform
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
@email = EmailMessage.find(params[:id])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module LetterThief
|
2
|
+
module ApplicationHelper
|
3
|
+
def parsed_body_html(email)
|
4
|
+
rendered = email.body_html
|
5
|
+
email.attachments.each do |attachment|
|
6
|
+
puts attachment.blob.metadata["cid"]
|
7
|
+
rendered.gsub!("cid:#{attachment.blob.metadata["cid"]}", main_app.rails_blob_path(attachment))
|
8
|
+
end
|
9
|
+
# autolinking can be implemented for text bodies
|
10
|
+
# rendered.gsub!(URI::Parser.new.make_regexp(%W[https http])) do |link|
|
11
|
+
# "<a href=\"#{ link }\">#{ link }</a>"
|
12
|
+
# end
|
13
|
+
rendered
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module LetterThief
|
2
|
+
class EmailMessage < ApplicationRecord
|
3
|
+
self.table_name = "letter_thief_email_messages"
|
4
|
+
|
5
|
+
has_many_attached :attachments
|
6
|
+
|
7
|
+
unless ActiveRecord::Base.connection.adapter_name.downcase.include?("postgresql")
|
8
|
+
serialize :to, coder: JSON, type: Array
|
9
|
+
serialize :from, coder: JSON, type: Array
|
10
|
+
serialize :sender, coder: JSON, type: Array
|
11
|
+
serialize :cc, coder: JSON, type: Array
|
12
|
+
serialize :bcc, coder: JSON, type: Array
|
13
|
+
end
|
14
|
+
|
15
|
+
def type
|
16
|
+
/html/.match?(content_type) ? "rich" : "plain"
|
17
|
+
end
|
18
|
+
|
19
|
+
def rich?
|
20
|
+
type == "rich"
|
21
|
+
end
|
22
|
+
|
23
|
+
def multipart?
|
24
|
+
body_text.present? && body_html.present?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module LetterThief
|
2
|
+
class EmailSearch
|
3
|
+
PAGE_SIZE = 20
|
4
|
+
|
5
|
+
attr_reader :query, :start_time, :end_time, :page, :total_count, :results, :has_next_page
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
@query = params[:query].to_s.strip
|
9
|
+
@start_time = params[:start_time]
|
10
|
+
@end_time = params[:end_time]
|
11
|
+
@page = params[:page].to_i
|
12
|
+
@page = 1 if @page < 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
scope = EmailMessage.order(intercepted_at: :desc)
|
17
|
+
|
18
|
+
if query.present?
|
19
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
20
|
+
scope = if adapter.include?("postgresql")
|
21
|
+
scope.where(<<~SQL.squish, q: "%#{query}%")
|
22
|
+
subject ILIKE :q
|
23
|
+
OR array_to_string("from", ',') ILIKE :q
|
24
|
+
OR array_to_string("to", ',') ILIKE :q
|
25
|
+
SQL
|
26
|
+
elsif adapter.include?("mysql")
|
27
|
+
scope.where("`subject` LIKE :q OR `from` LIKE :q OR `to` LIKE :q", q: "%#{query}%")
|
28
|
+
else
|
29
|
+
scope.where('subject LIKE :q OR "from" LIKE :q OR "to" LIKE :q', q: "%#{query}%")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
scope = scope.where("intercepted_at >= ?", parse_datetime(start_time)) if start_time.present?
|
34
|
+
scope = scope.where("intercepted_at <= ?", parse_datetime(end_time)) if end_time.present?
|
35
|
+
|
36
|
+
@total_count = scope.count
|
37
|
+
@results = scope.limit(PAGE_SIZE).offset((page - 1) * PAGE_SIZE)
|
38
|
+
@has_next_page = total_count > page * PAGE_SIZE
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def parse_datetime(value)
|
45
|
+
DateTime.parse(value) if value.present?
|
46
|
+
rescue ArgumentError
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html style="height:100%">
|
3
|
+
<head>
|
4
|
+
<title>Letter thief</title>
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
<%= csp_meta_tag %>
|
8
|
+
|
9
|
+
<%= yield :head %>
|
10
|
+
|
11
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
12
|
+
</head>
|
13
|
+
<body style="height:100%">
|
14
|
+
<main class="container" style="height:100%">
|
15
|
+
<%= yield %>
|
16
|
+
</main>
|
17
|
+
</body>
|
18
|
+
</html>
|
@@ -0,0 +1,92 @@
|
|
1
|
+
<h1>📬 Intercepted Emails</h1>
|
2
|
+
|
3
|
+
<form method="get" style="margin-bottom: 1rem;">
|
4
|
+
<fieldset class="grid">
|
5
|
+
<label>
|
6
|
+
Query
|
7
|
+
<input
|
8
|
+
name="query"
|
9
|
+
value="<%= h(@search.query) %>"
|
10
|
+
placeholder="Search by from, to, or subject"/>
|
11
|
+
</label>
|
12
|
+
|
13
|
+
<label>
|
14
|
+
From Time
|
15
|
+
<input
|
16
|
+
type="datetime-local"
|
17
|
+
name="start_time"
|
18
|
+
value="<%= @search.start_time %>"/>
|
19
|
+
</label>
|
20
|
+
|
21
|
+
<label>
|
22
|
+
To Time
|
23
|
+
<input
|
24
|
+
type="datetime-local"
|
25
|
+
name="end_time"
|
26
|
+
value="<%= @search.end_time %>"/>
|
27
|
+
</label>
|
28
|
+
|
29
|
+
<input type="submit" value="Search" style="margin-top: 1.7rem"/>
|
30
|
+
|
31
|
+
</fieldset>
|
32
|
+
</form>
|
33
|
+
|
34
|
+
|
35
|
+
<% if @search.total_count > 0 %>
|
36
|
+
<p><%= pluralize(@search.total_count, "email") %> found</p>
|
37
|
+
|
38
|
+
<table role="grid">
|
39
|
+
<thead>
|
40
|
+
<tr>
|
41
|
+
<th>From</th>
|
42
|
+
<th>To</th>
|
43
|
+
<th>Subject</th>
|
44
|
+
<th>Time</th>
|
45
|
+
</tr>
|
46
|
+
</thead>
|
47
|
+
<tbody>
|
48
|
+
<% @search.results.each do |email| %>
|
49
|
+
<tr>
|
50
|
+
<td><%= email.from&.join(", ") %></td>
|
51
|
+
<td><%= email.to&.join(", ") %></td>
|
52
|
+
<td>
|
53
|
+
<a href="<%= letter_thief.email_message_path(email) %>">
|
54
|
+
<%= email.subject.presence || "(no subject)" %>
|
55
|
+
</a>
|
56
|
+
</td>
|
57
|
+
<td><%= email.intercepted_at.strftime("%Y-%m-%d %H:%M") %></td>
|
58
|
+
</tr>
|
59
|
+
<% end %>
|
60
|
+
</tbody>
|
61
|
+
</table>
|
62
|
+
|
63
|
+
<nav aria-label="Pagination">
|
64
|
+
<ul style="display: flex; gap: 1rem; list-style: none; padding-left: 0;">
|
65
|
+
<% if @search.page > 1 %>
|
66
|
+
<li>
|
67
|
+
<%= link_to "← Previous", letter_thief.email_messages_path(
|
68
|
+
query: @search.query,
|
69
|
+
start_time: @search.start_time,
|
70
|
+
end_time: @search.end_time,
|
71
|
+
page: @search.page - 1
|
72
|
+
) %>
|
73
|
+
</li>
|
74
|
+
<% end %>
|
75
|
+
|
76
|
+
<li>Page <%= @search.page %></li>
|
77
|
+
|
78
|
+
<% if @search.has_next_page %>
|
79
|
+
<li>
|
80
|
+
<%= link_to "Next →", letter_thief.email_messages_path(
|
81
|
+
query: @search.query,
|
82
|
+
start_time: @search.start_time,
|
83
|
+
end_time: @search.end_time,
|
84
|
+
page: @search.page + 1
|
85
|
+
) %>
|
86
|
+
</li>
|
87
|
+
<% end %>
|
88
|
+
</ul>
|
89
|
+
</nav>
|
90
|
+
<% else %>
|
91
|
+
<p>No emails found.</p>
|
92
|
+
<% end %>
|
@@ -0,0 +1,124 @@
|
|
1
|
+
<p><a href="<%= letter_thief.email_messages_path %>">← Back to Inbox 📬</a></p>
|
2
|
+
|
3
|
+
<section id="message_headers">
|
4
|
+
<dl>
|
5
|
+
<dt>Intercepted at:</dt>
|
6
|
+
<dd><%= @email.intercepted_at.strftime("%b %e, %Y %I:%M %p") %></dd>
|
7
|
+
<% if @email.from.present? %>
|
8
|
+
<dt>From:</dt>
|
9
|
+
<dd><%= @email.from.join(", ") %></dd>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<% if @email.sender.present? %>
|
13
|
+
<dt>Sender:</dt>
|
14
|
+
<dd><%= @email.sender.join(", ") %></dd>
|
15
|
+
<% end %>
|
16
|
+
|
17
|
+
<% if @email.to.present? %>
|
18
|
+
<dt>To:</dt>
|
19
|
+
<dd><%= @email.to.join(", ") %></dd>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
<dt>Subject:</dt>
|
23
|
+
<dd><strong><%= h(@email.subject || "(no subject)") %></strong></dd>
|
24
|
+
|
25
|
+
<% if @email.cc.present? %>
|
26
|
+
<dt>CC:</dt>
|
27
|
+
<dd><%= @email.cc.join(", ") %></dd>
|
28
|
+
<% end %>
|
29
|
+
|
30
|
+
<% if @email.bcc.present? %>
|
31
|
+
<dt>BCC:</dt>
|
32
|
+
<dd><%= @email.bcc.join(", ") %></dd>
|
33
|
+
<% end %>
|
34
|
+
|
35
|
+
<% if @email.attachments.any? %>
|
36
|
+
<dt>Attachments:</dt>
|
37
|
+
<dd>
|
38
|
+
<% @email.attachments.each do |file| %>
|
39
|
+
<%= link_to file.filename, main_app.rails_blob_path(file, disposition: "attachment") %>
|
40
|
+
<% end %>
|
41
|
+
</dd>
|
42
|
+
<% end %>
|
43
|
+
</dl>
|
44
|
+
</section>
|
45
|
+
|
46
|
+
<hr>
|
47
|
+
|
48
|
+
<% if @email.multipart? %>
|
49
|
+
<nav>
|
50
|
+
<ul>
|
51
|
+
<li data-version-toggle="html" style="display: none">
|
52
|
+
<a href="#" role="button" class="outline">View HTML version</a></li>
|
53
|
+
<li data-version-toggle="text"><a href="#" role="button" class="outline">View plain text version</a></li>
|
54
|
+
</ul>
|
55
|
+
</nav>
|
56
|
+
<% end %>
|
57
|
+
|
58
|
+
<section id="email_body" style="height:100%">
|
59
|
+
<% if @email.body_text.present? %>
|
60
|
+
<% if @email.rich? %>
|
61
|
+
<iframe
|
62
|
+
id="text-version"
|
63
|
+
class="email-version"
|
64
|
+
srcdoc="<base target='_top'><%= h(@email.body_text) %>">
|
65
|
+
</iframe>
|
66
|
+
<% else %>
|
67
|
+
<pre class="email-version" id="text-version" style="display: <%= @email.multipart? ? 'none' : 'block' %>"><%= h(@email.body_text) %></pre>
|
68
|
+
<% end %>
|
69
|
+
<% end %>
|
70
|
+
|
71
|
+
<% if @email.body_html.present? %>
|
72
|
+
<iframe
|
73
|
+
id="html-version"
|
74
|
+
class="email-version"
|
75
|
+
srcdoc="<base target='_top'><%= parsed_body_html(@email) %>">
|
76
|
+
</iframe>
|
77
|
+
<% end %>
|
78
|
+
</section>
|
79
|
+
|
80
|
+
<script>
|
81
|
+
function toggleVersions(version, otherVersion) {
|
82
|
+
document.querySelector(`[data-version-toggle="${version}"]`).addEventListener('click', event => {
|
83
|
+
event.preventDefault()
|
84
|
+
document.querySelector(`[data-version-toggle="${version}"]`).style.display = "none";
|
85
|
+
document.querySelector(`[data-version-toggle="${otherVersion}"]`).style.display = "block";
|
86
|
+
|
87
|
+
document.getElementById(`${version}-version`).style.display = 'block'
|
88
|
+
document.getElementById(`${otherVersion}-version`).style.display = 'none'
|
89
|
+
});
|
90
|
+
}
|
91
|
+
|
92
|
+
toggleVersions("text", "html");
|
93
|
+
toggleVersions("html", "text");
|
94
|
+
</script>
|
95
|
+
|
96
|
+
<style>
|
97
|
+
#message_headers {
|
98
|
+
dt {
|
99
|
+
color: #7f7f7f;
|
100
|
+
text-align: right;
|
101
|
+
font-weight: bold;
|
102
|
+
}
|
103
|
+
|
104
|
+
dl {
|
105
|
+
display: grid;
|
106
|
+
grid-template-columns: max-content auto;
|
107
|
+
row-gap: 0.5rem;
|
108
|
+
font-size: 0.95rem;
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
pre {
|
113
|
+
border: 1px solid #ddd;
|
114
|
+
padding: 1rem;
|
115
|
+
white-space: pre-wrap;
|
116
|
+
font-family: monospace;
|
117
|
+
}
|
118
|
+
|
119
|
+
iframe {
|
120
|
+
border: 0;
|
121
|
+
width: 100%;
|
122
|
+
height: 100%;
|
123
|
+
}
|
124
|
+
</style>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module LetterThief
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include ActiveRecord::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
|
11
|
+
def copy_migration
|
12
|
+
migration_template "create_email_messages.rb.erb",
|
13
|
+
"db/migrate/create_letter_thief_email_messages.rb",
|
14
|
+
migration_version: migration_version
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def migration_version
|
20
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class CreateLetterThiefEmailMessages < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
4
|
+
|
5
|
+
create_table :letter_thief_email_messages do |t|
|
6
|
+
if adapter.include?("postgresql")
|
7
|
+
t.string :to, array: true
|
8
|
+
t.string :from, array: true
|
9
|
+
t.string :sender, array: true
|
10
|
+
t.string :cc, array: true
|
11
|
+
t.string :bcc, array: true
|
12
|
+
else
|
13
|
+
t.text :to
|
14
|
+
t.text :from
|
15
|
+
t.text :sender
|
16
|
+
t.text :cc
|
17
|
+
t.text :bcc
|
18
|
+
end
|
19
|
+
|
20
|
+
t.string :subject
|
21
|
+
t.text :body_text
|
22
|
+
t.text :body_html
|
23
|
+
t.text :headers
|
24
|
+
t.text :raw_message
|
25
|
+
t.string :content_type
|
26
|
+
t.datetime :intercepted_at
|
27
|
+
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
add_index :letter_thief_email_messages, :intercepted_at
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
|
3
|
+
module LetterThief
|
4
|
+
class DeliveryMethod
|
5
|
+
def initialize(options = {})
|
6
|
+
end
|
7
|
+
|
8
|
+
# this is not really true, we don't know if it's the last email, but for the moment this should do.
|
9
|
+
def deliver!(mail)
|
10
|
+
require "launchy"
|
11
|
+
::Launchy.open(LetterThief::Engine.routes.url_helpers.email_message_url(LetterThief::EmailMessage.last, Rails.configuration.action_mailer.default_url_options))
|
12
|
+
rescue LoadError
|
13
|
+
puts "WARNING: LetterThief requires the 'launchy' gem to open the email in a web browser. Add it to your Gemfile."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "letter_thief/interceptor"
|
2
|
+
require "letter_thief/delivery_method"
|
3
|
+
|
4
|
+
module LetterThief
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace LetterThief
|
7
|
+
|
8
|
+
initializer "letter_thief.add_interceptor" do
|
9
|
+
ActiveSupport.on_load(:action_mailer) do
|
10
|
+
ActionMailer::Base.register_interceptor(LetterThief::Interceptor)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer "letter_thief.add_delivery_method" do
|
15
|
+
ActiveSupport.on_load(:action_mailer) do
|
16
|
+
ActionMailer::Base.add_delivery_method(:letter_thief, LetterThief::DeliveryMethod)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module LetterThief
|
2
|
+
class Interceptor
|
3
|
+
def self.delivering_email(mail)
|
4
|
+
email = EmailMessage.create!(
|
5
|
+
to: mail.to,
|
6
|
+
from: mail.from,
|
7
|
+
sender: mail.sender,
|
8
|
+
cc: mail.cc,
|
9
|
+
bcc: mail.bcc,
|
10
|
+
subject: mail.subject,
|
11
|
+
body_text: mail.text_part&.decoded || mail.body.decoded,
|
12
|
+
body_html: mail.html_part&.decoded,
|
13
|
+
headers: mail.header.to_s,
|
14
|
+
raw_message: mail.to_s,
|
15
|
+
content_type: mail.content_type,
|
16
|
+
intercepted_at: Time.current
|
17
|
+
)
|
18
|
+
|
19
|
+
Array(mail.attachments).each do |attachment|
|
20
|
+
ar_attachment = email.attachments.attach(
|
21
|
+
io: StringIO.new(attachment.body.decoded),
|
22
|
+
filename: attachment.filename,
|
23
|
+
content_type: attachment.mime_type
|
24
|
+
).last
|
25
|
+
ar_attachment.blob.metadata["cid"] = attachment.cid
|
26
|
+
ar_attachment.blob.save!
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
Rails.logger.error("[LetterThief] Failed to store intercepted email: #{e.message}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/letter_thief.rb
ADDED
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: letter_thief
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alessandro Rodi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-03-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
description: LetterThief allows you to record sent emails in your database.
|
28
|
+
email:
|
29
|
+
- rodi@hey.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- Rakefile
|
36
|
+
- app/controllers/letter_thief/application_controller.rb
|
37
|
+
- app/controllers/letter_thief/email_messages_controller.rb
|
38
|
+
- app/helpers/letter_thief/application_helper.rb
|
39
|
+
- app/jobs/letter_thief/application_job.rb
|
40
|
+
- app/models/letter_thief/application_record.rb
|
41
|
+
- app/models/letter_thief/email_message.rb
|
42
|
+
- app/models/letter_thief/email_search.rb
|
43
|
+
- app/views/layouts/letter_thief/application.html.erb
|
44
|
+
- app/views/letter_thief/email_messages/index.html.erb
|
45
|
+
- app/views/letter_thief/email_messages/show.html.erb
|
46
|
+
- config/routes.rb
|
47
|
+
- lib/generators/letter_thief/install_generator.rb
|
48
|
+
- lib/generators/letter_thief/templates/create_email_messages.rb.erb
|
49
|
+
- lib/letter_thief.rb
|
50
|
+
- lib/letter_thief/delivery_method.rb
|
51
|
+
- lib/letter_thief/engine.rb
|
52
|
+
- lib/letter_thief/interceptor.rb
|
53
|
+
- lib/letter_thief/version.rb
|
54
|
+
- lib/tasks/letter_thief_tasks.rake
|
55
|
+
homepage: https://github.com/coorasse/letter_thief
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata:
|
59
|
+
homepage_uri: https://github.com/coorasse/letter_thief
|
60
|
+
source_code_uri: https://github.com/coorasse/letter_thief
|
61
|
+
changelog_uri: https://github.com/coorasse/letter_thief/blob/main/CHANGELOG.md
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubygems_version: 3.3.27
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Log emails in Rails and visualize them.
|
81
|
+
test_files: []
|