letter_thief 0.1.1 → 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: 39bd9fdf0035b1bf8b6ffdaadb8dd5df7eca96b2d9bb03c7b0a9db6c0dc80667
4
- data.tar.gz: 213afbdd43435763f9b2c40e5d62f3190a99ac96e8d2e97cffad46165ba43170
3
+ metadata.gz: f3b860cc3ad45c5e200e2cb4b5d9610fd6a2e8438131de9b24dc2c8c902390f5
4
+ data.tar.gz: d5ce85e83a08a78010c79bfd3ca77f14f3d3c3f8e3632458bd0eb0ffb0886f53
5
5
  SHA512:
6
- metadata.gz: c84699514d01c43ee6fe829a98fab49642c682fbeab3cd4aa1b683b647426a37c8d3dcb5ab49e8392afa81990bc6dc7fcfae5d0c49d617003c6d9b9e637053a6
7
- data.tar.gz: eb1d35d8fec2b7130d15efd7ab616598a74d2e50d57da32a32b294292cad9215ad422218c7c0ed52940a3ff8a09059cd74de5df9cb3835b0d42105969fdfbf68
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,13 @@ 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
+
10
+ content_security_policy do |policy|
11
+ policy.style_src :self, :https, :unsafe_inline
12
+ end
13
+
7
14
  PAGE_SIZE = 20
8
15
 
9
16
  def index
@@ -11,7 +18,27 @@ module LetterThief
11
18
  end
12
19
 
13
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
14
35
  @email = EmailMessage.find(params[:id])
15
36
  end
37
+
38
+ private
39
+
40
+ def turn_off_csp_nonce_generation
41
+ request.content_security_policy_nonce_directives = []
42
+ end
16
43
  end
17
44
  end
@@ -2,7 +2,12 @@ module LetterThief
2
2
  class EmailMessage < ApplicationRecord
3
3
  self.table_name = "letter_thief_email_messages"
4
4
 
5
- has_many_attached :attachments
5
+ connects_to(**LetterThief.connects_to) if LetterThief.connects_to
6
+
7
+ if LetterThief.activestorage_available?
8
+ has_many_attached :attachments
9
+ has_one_attached :raw_email
10
+ end
6
11
 
7
12
  unless ActiveRecord::Base.connection.adapter_name.downcase.include?("postgresql")
8
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,6 +1,14 @@
1
1
  <% content_for :title, "Letter Thief" %>
2
2
 
3
- <h1>📬 Intercepted Emails</h1>
3
+ <h1>📬 Intercepted Emails (Outbox)</h1>
4
+
5
+ <p>
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 %>
11
+ </p>
4
12
 
5
13
  <form method="get" style="margin-bottom: 1rem;">
6
14
  <fieldset class="grid">
@@ -33,7 +41,6 @@
33
41
  </fieldset>
34
42
  </form>
35
43
 
36
-
37
44
  <% if @search.total_count > 0 %>
38
45
  <p><%= pluralize(@search.total_count, "email") %> found</p>
39
46
 
@@ -44,21 +51,12 @@
44
51
  <th>To</th>
45
52
  <th>Subject</th>
46
53
  <th>Time</th>
54
+ <th>Size</th>
55
+ <th></th>
47
56
  </tr>
48
57
  </thead>
49
58
  <tbody>
50
- <% @search.results.each do |email| %>
51
- <tr>
52
- <td><%= email.from&.join(", ") %></td>
53
- <td><%= email.to&.join(", ") %></td>
54
- <td>
55
- <a href="<%= email_message_path(email) %>">
56
- <%= email.subject.presence || "(no subject)" %>
57
- </a>
58
- </td>
59
- <td><%= email.intercepted_at.strftime("%Y-%m-%d %H:%M") %></td>
60
- </tr>
61
- <% end %>
59
+ <%= render @search.results %>
62
60
  </tbody>
63
61
  </table>
64
62
 
@@ -89,6 +87,22 @@
89
87
  <% end %>
90
88
  </ul>
91
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>
92
106
  <% else %>
93
107
  <p>No emails found.</p>
94
108
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <% content_for :title, "Message ##{@email.id}" %>
2
2
 
3
- <p><a href="<%= email_messages_path %>">&larr; Back to Inbox 📬</a></p>
3
+ <p><a href="<%= email_messages_path %>">&larr; Back to Outbox 📬</a></p>
4
4
 
5
5
  <section id="message_headers">
6
6
  <dl>
@@ -33,18 +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>
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 %>
52
+ <% end %>
46
53
  </section>
47
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
+
48
59
  <hr>
49
60
 
50
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
@@ -21,7 +21,6 @@ class CreateLetterThiefEmailMessages < ActiveRecord::Migration<%= migration_vers
21
21
  t.text :body_text
22
22
  t.text :body_html
23
23
  t.text :headers
24
- t.text :raw_message
25
24
  t.string :content_type
26
25
  t.datetime :intercepted_at
27
26
 
@@ -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.1.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/letter_thief.rb CHANGED
@@ -2,5 +2,18 @@ require "letter_thief/version"
2
2
  require "letter_thief/engine"
3
3
 
4
4
  module LetterThief
5
- # Your code goes here...
5
+ mattr_accessor :connects_to
6
+
7
+ def self.used_activestorage_space
8
+ ActiveStorage::Blob
9
+ .joins(:attachments)
10
+ .where(active_storage_attachments: {
11
+ record_type: "LetterThief::EmailMessage"
12
+ }).sum(:byte_size)
13
+ end
14
+
15
+ def self.activestorage_available?
16
+ defined?(ActiveStorage) &&
17
+ ActiveRecord::Base.connection.table_exists?("active_storage_attachments")
18
+ end
6
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.1.1
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-04 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,32 +0,0 @@
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