letter_thief 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967473d3c9121f2d1d82d1d477284f251b0b19666f2511cdde5a1123b58255c2
4
- data.tar.gz: 174d09ccdf13d992281e8533e565646069f494f36fd49b221dd7c1e665b9e1a0
3
+ metadata.gz: f3b860cc3ad45c5e200e2cb4b5d9610fd6a2e8438131de9b24dc2c8c902390f5
4
+ data.tar.gz: d5ce85e83a08a78010c79bfd3ca77f14f3d3c3f8e3632458bd0eb0ffb0886f53
5
5
  SHA512:
6
- metadata.gz: 6233fbd351a7e103122aa373593bd2acab90a4143dc8fb32c31ef71642255bf2a7a796363ea4fe32daca5d0c4c2adb1677fc6a589f39da36bf90b84420838de2
7
- data.tar.gz: ccf721f2572308ad7e592bc9afeb1c5b902fd27d30f55e97aa414068610891ead8fe1d56516a68bbadc70499674187dc482447d41ad9f813adcecfe79a5b847e
6
+ metadata.gz: f698e01a0984e8d8f4404892831ae71275d5fd1ae35077a5c0b58ce1cb7ab1bd1eacf2d9a567a47d09ad4ea017ab042ae5ab9b0aefbadf473e224a656b043c8e
7
+ data.tar.gz: 5f817d2ddbd8260fe90b9aab6c9c7433361df14f0944e45ff3fb1c0751b0d4e931482e9f4cccba2f49bdaa13b43036ca6ce3162280c513c4f1df7fb5174e5461
data/README.md CHANGED
@@ -52,7 +52,7 @@ Mount the engine in your routes, protecting it.
52
52
  The code below might be different depending on how you authenticate your users.
53
53
 
54
54
  ```ruby
55
- authenticate :user, ->(user) { sys_manager&.administrator? } do
55
+ authenticate :user, ->(user) { user&.administrator? } do
56
56
  mount LetterThief::Engine => "/letter_thief"
57
57
  end
58
58
  ```
@@ -4,6 +4,9 @@ module LetterThief
4
4
  class EmailMessagesController < ApplicationController
5
5
  layout "letter_thief/application"
6
6
 
7
+ before_action :turn_off_csp_nonce_generation
8
+ before_action :set_email, only: [:show, :destroy]
9
+
7
10
  content_security_policy do |policy|
8
11
  policy.style_src :self, :https, :unsafe_inline
9
12
  end
@@ -15,7 +18,27 @@ module LetterThief
15
18
  end
16
19
 
17
20
  def show
21
+ end
22
+
23
+ def destroy
24
+ redirect_to email_messages_path if @email.destroy
25
+ end
26
+
27
+ def destroy_all
28
+ EmailMessage.destroy_all
29
+ redirect_to email_messages_path, notice: "All email messages have been deleted"
30
+ end
31
+
32
+ private
33
+
34
+ def set_email
18
35
  @email = EmailMessage.find(params[:id])
19
36
  end
37
+
38
+ private
39
+
40
+ def turn_off_csp_nonce_generation
41
+ request.content_security_policy_nonce_directives = []
42
+ end
20
43
  end
21
44
  end
@@ -4,8 +4,10 @@ module LetterThief
4
4
 
5
5
  connects_to(**LetterThief.connects_to) if LetterThief.connects_to
6
6
 
7
- has_many_attached :attachments
8
- has_one_attached :raw_email
7
+ if LetterThief.activestorage_available?
8
+ has_many_attached :attachments
9
+ has_one_attached :raw_email
10
+ end
9
11
 
10
12
  unless ActiveRecord::Base.connection.adapter_name.downcase.include?("postgresql")
11
13
  serialize :to, coder: JSON, type: Array
@@ -42,7 +42,7 @@ module LetterThief
42
42
  private
43
43
 
44
44
  def parse_datetime(value)
45
- DateTime.parse(value) if value.present?
45
+ Time.zone.parse(value) if value.present?
46
46
  rescue ArgumentError
47
47
  nil
48
48
  end
@@ -0,0 +1,25 @@
1
+ <%# locals:(email_message:) %>
2
+ <tr>
3
+ <td>
4
+ <%= email_message.from&.join(", ") %>
5
+ </td>
6
+ <td>
7
+ <%= email_message.to&.join(", ") %>
8
+ </td>
9
+ <td>
10
+ <%= link_to email_message.subject.presence || "(no subject)", email_message_path(email_message) %>
11
+ </td>
12
+ <td>
13
+ <%= email_message.intercepted_at.strftime("%Y-%m-%d %H:%M") %>
14
+ </td>
15
+ <td>
16
+ <% if @email.respond_to?(:raw_email) %>
17
+ <%= number_to_human_size(email_message.raw_email.byte_size) %>
18
+ <% else %>
19
+ -
20
+ <% end %>
21
+ </td>
22
+ <td>
23
+ <%= button_to 'Delete', email_message_path(email_message), method: :delete, class: 'outline contrast' %>
24
+ </td>
25
+ </tr>
@@ -1,9 +1,15 @@
1
1
  <% content_for :title, "Letter Thief" %>
2
2
 
3
3
  <h1>📬 Intercepted Emails (Outbox)</h1>
4
+
4
5
  <p>
5
- Currently occupying <%= number_to_human_size(LetterThief.used_activestorage_space)%>
6
+ <% if LetterThief.activestorage_available? %>
7
+ Currently occupying <%= number_to_human_size(LetterThief.used_activestorage_space)%>
8
+ <% else %>
9
+ Attachments are not saved. In order to save the attachments you need to setup ActiveStorage.
10
+ <% end %>
6
11
  </p>
12
+
7
13
  <form method="get" style="margin-bottom: 1rem;">
8
14
  <fieldset class="grid">
9
15
  <label>
@@ -35,7 +41,6 @@
35
41
  </fieldset>
36
42
  </form>
37
43
 
38
-
39
44
  <% if @search.total_count > 0 %>
40
45
  <p><%= pluralize(@search.total_count, "email") %> found</p>
41
46
 
@@ -47,22 +52,11 @@
47
52
  <th>Subject</th>
48
53
  <th>Time</th>
49
54
  <th>Size</th>
55
+ <th></th>
50
56
  </tr>
51
57
  </thead>
52
58
  <tbody>
53
- <% @search.results.each do |email| %>
54
- <tr>
55
- <td><%= email.from&.join(", ") %></td>
56
- <td><%= email.to&.join(", ") %></td>
57
- <td>
58
- <a href="<%= email_message_path(email) %>">
59
- <%= email.subject.presence || "(no subject)" %>
60
- </a>
61
- </td>
62
- <td><%= email.intercepted_at.strftime("%Y-%m-%d %H:%M") %></td>
63
- <td><%= number_to_human_size(email.raw_email.byte_size) %></td>
64
- </tr>
65
- <% end %>
59
+ <%= render @search.results %>
66
60
  </tbody>
67
61
  </table>
68
62
 
@@ -93,6 +87,22 @@
93
87
  <% end %>
94
88
  </ul>
95
89
  </nav>
90
+
91
+ <%= button_to "Delete All Messages",
92
+ destroy_all_email_messages_path,
93
+ method: :delete,
94
+ class: "secondary outline",
95
+ onclick: "return confirmDeleteAll(event)" %>
96
+
97
+ <script>
98
+ function confirmDeleteAll(event) {
99
+ event.preventDefault();
100
+ if (window.confirm('Are you sure you want to delete all messages? This action cannot be undone.')) {
101
+ event.target.closest('form').submit();
102
+ }
103
+ return false;
104
+ }
105
+ </script>
96
106
  <% else %>
97
107
  <p>No emails found.</p>
98
108
  <% end %>
@@ -33,21 +33,29 @@
33
33
  <dt>BCC:</dt>
34
34
  <dd><%= @email.bcc.join(", ") %></dd>
35
35
  <% end %>
36
-
37
- <% if @email.attachments.any? %>
38
- <dt>Attachments:</dt>
39
- <dd>
40
- <% @email.attachments.each do |file| %>
41
- <%= link_to file.filename, main_app.rails_blob_path(file, disposition: "attachment") %>
36
+ <% if @email.respond_to?(:attachments) %>
37
+ <% if @email.attachments.any? %>
38
+ <dt>Attachments:</dt>
39
+ <dd>
40
+ <% @email.attachments.each do |file| %>
41
+ <%= link_to file.filename, main_app.rails_blob_path(file, disposition: "attachment") %>
42
+ <% end %>
43
+ </dd>
42
44
  <% end %>
43
- </dd>
44
45
  <% end %>
45
46
  </dl>
46
- <% if @email.raw_email.attached? %>
47
- <%= link_to "Download", main_app.rails_blob_path(@email.raw_email, disposition: "attachment") %>
47
+
48
+ <% if @email.respond_to?(:raw_email) %>
49
+ <% if @email.raw_email.attached? %>
50
+ <%= link_to "Download", main_app.rails_blob_path(@email.raw_email, disposition: "attachment") %>
51
+ <% end %>
48
52
  <% end %>
49
53
  </section>
50
54
 
55
+ <% if !LetterThief.activestorage_available? %>
56
+ <p>Attachments are not saved. In order to save the attachments you need to setup ActiveStorage.</p>
57
+ <% end %>
58
+
51
59
  <hr>
52
60
 
53
61
  <% if @email.multipart? %>
data/config/routes.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  LetterThief::Engine.routes.draw do
2
- resources :email_messages, only: [:index, :show]
2
+ resources :email_messages, only: [:index, :show, :destroy] do
3
+ delete :destroy_all, on: :collection
4
+ end
3
5
  root "email_messages#index"
4
6
  end
@@ -1,13 +1,13 @@
1
- require "letter_thief/interceptor"
1
+ require "letter_thief/observer"
2
2
  require "letter_thief/delivery_method"
3
3
 
4
4
  module LetterThief
5
5
  class Engine < ::Rails::Engine
6
6
  isolate_namespace LetterThief
7
7
 
8
- initializer "letter_thief.add_interceptor" do
8
+ initializer "letter_thief.add_observer" do
9
9
  ActiveSupport.on_load(:action_mailer) do
10
- ActionMailer::Base.register_interceptor(LetterThief::Interceptor)
10
+ ActionMailer::Base.register_observer(LetterThief::Observer)
11
11
  end
12
12
  end
13
13
 
@@ -0,0 +1,40 @@
1
+ module LetterThief
2
+ class Observer
3
+ def self.delivered_email(mail)
4
+ string_io = StringIO.new(mail.to_s)
5
+ email = EmailMessage.create!(
6
+ to: mail.to,
7
+ from: mail.from,
8
+ sender: mail.sender,
9
+ cc: mail.cc,
10
+ bcc: mail.bcc,
11
+ subject: mail.subject,
12
+ body_text: mail.text_part&.decoded || mail.body.decoded,
13
+ body_html: mail.html_part&.decoded,
14
+ headers: mail.header.to_s,
15
+ content_type: mail.content_type,
16
+ intercepted_at: Time.current
17
+ )
18
+
19
+ if LetterThief.activestorage_available?
20
+ Array(mail.attachments).each do |attachment|
21
+ ar_attachment = email.attachments.attach(
22
+ io: StringIO.new(attachment.body.decoded),
23
+ filename: attachment.filename,
24
+ content_type: attachment.mime_type
25
+ ).last
26
+ ar_attachment.blob.metadata["cid"] = attachment.cid
27
+ ar_attachment.blob.save!
28
+ end
29
+
30
+ email.raw_email.attach(
31
+ io: string_io,
32
+ filename: "message-#{email.id}.eml",
33
+ content_type: "message/rfc822"
34
+ )
35
+ end
36
+ rescue => e
37
+ Rails.logger.error("[LetterThief] Failed to store observed email: #{e.message}")
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module LetterThief
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/letter_thief.rb CHANGED
@@ -11,4 +11,9 @@ module LetterThief
11
11
  record_type: "LetterThief::EmailMessage"
12
12
  }).sum(:byte_size)
13
13
  end
14
+
15
+ def self.activestorage_available?
16
+ defined?(ActiveStorage) &&
17
+ ActiveRecord::Base.connection.table_exists?("active_storage_attachments")
18
+ end
14
19
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: letter_thief
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-06 00:00:00.000000000 Z
11
+ date: 2025-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -41,6 +41,7 @@ files:
41
41
  - app/models/letter_thief/email_message.rb
42
42
  - app/models/letter_thief/email_search.rb
43
43
  - app/views/layouts/letter_thief/application.html.erb
44
+ - app/views/letter_thief/email_messages/_email_message.html.erb
44
45
  - app/views/letter_thief/email_messages/index.html.erb
45
46
  - app/views/letter_thief/email_messages/show.html.erb
46
47
  - config/routes.rb
@@ -49,7 +50,7 @@ files:
49
50
  - lib/letter_thief.rb
50
51
  - lib/letter_thief/delivery_method.rb
51
52
  - lib/letter_thief/engine.rb
52
- - lib/letter_thief/interceptor.rb
53
+ - lib/letter_thief/observer.rb
53
54
  - lib/letter_thief/version.rb
54
55
  - lib/tasks/letter_thief_tasks.rake
55
56
  homepage: https://github.com/coorasse/letter_thief
@@ -1,38 +0,0 @@
1
- module LetterThief
2
- class Interceptor
3
- def self.delivering_email(mail)
4
- string_io = StringIO.new(mail.to_s)
5
- email = EmailMessage.create!(
6
- to: mail.to,
7
- from: mail.from,
8
- sender: mail.sender,
9
- cc: mail.cc,
10
- bcc: mail.bcc,
11
- subject: mail.subject,
12
- body_text: mail.text_part&.decoded || mail.body.decoded,
13
- body_html: mail.html_part&.decoded,
14
- headers: mail.header.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
-
29
- email.raw_email.attach(
30
- io: string_io,
31
- filename: "message-#{email.id}.eml",
32
- content_type: "message/rfc822"
33
- )
34
- rescue => e
35
- Rails.logger.error("[LetterThief] Failed to store intercepted email: #{e.message}")
36
- end
37
- end
38
- end