mail_dude 0.1.2 → 0.1.4

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: 1bf22950ae8ba64ae0dc2176e5a5256fba2b7b1e0387ff24b421c3fa656fa205
4
- data.tar.gz: 4f518d5ed49a686666e7af69d0b480fac69767af2c685626b0771fa93e2aebcb
3
+ metadata.gz: 8ba107ba98ca78fe89c3e34585b6d49fe14d4e89f88dbfd587d62b95e0a651d5
4
+ data.tar.gz: 7a0cc62678dad96f2ecb50eaa4a6a708a451f76563e08488aee8897a66289bfb
5
5
  SHA512:
6
- metadata.gz: 2ed12acd960afb2984cae6b80a707bd7d58a8d2e67f3bade8a238cee89df99ee9f1f989f2a1d67edf3e72d69a18c62380cb7e0abd6e73900ae7317a729225757
7
- data.tar.gz: d8ded531449368b283d0341751331c13cdd1f74ce14207f697f8a971a5b60d02075c1fafde5e8886eff21171abffbebf8934a8540834d2e41b9bd4e5c2bc53dd
6
+ metadata.gz: c3ae2168b7602568b0c6ac480b8b1c2221d3bff427934c1029a103df9d808c6a8c0a8038704949fbe5acc00ad9571ec96f10cd0f6bbf070ba841cddf64bf57ec
7
+ data.tar.gz: a0d5e30232fb7d383597cf34f857880e6a06510f7e230c7f838c6062bce1a1f4ceeb1281a9a7d8050ddab87a9b354f76dfedf30e1be1be67b2f6d98b1768cfe6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ - Allow sandboxed previews to load same-origin inline attachments.
6
+ - Serialize Action Mailer inline attachments from captured raw source.
7
+
8
+ ## 0.1.3
9
+
10
+ - Fix CID image rendering for Content-ID attachments without inline disposition.
11
+ - Strip attachment payloads from stored raw source when attachment capture is disabled.
12
+
3
13
  ## 0.1.2
4
14
 
5
15
  - Add official Ruby 3.1 support.
@@ -9,6 +9,5 @@
9
9
  class="mail-dude-message-html-frame"
10
10
  name="mail-dude-html-frame"
11
11
  src="<%= html_message_path(presenter.id) %>"
12
- sandbox=""
12
+ sandbox="allow-same-origin"
13
13
  title="HTML email preview"></iframe>
14
-
@@ -2,6 +2,26 @@
2
2
 
3
3
  module MailDude
4
4
  class AttachmentLocator
5
+ class << self
6
+ def attachment_part?(part)
7
+ !part.multipart? && (part.attachment? || part.filename.present? || inline_renderable_part?(part))
8
+ end
9
+
10
+ def content_disposition(part)
11
+ part.content_disposition.to_s.split(';').first.to_s.downcase
12
+ end
13
+
14
+ def inline_renderable_part?(part)
15
+ inline_part?(part) || part.content_id.present?
16
+ end
17
+
18
+ private
19
+
20
+ def inline_part?(part)
21
+ content_disposition(part) == 'inline' && part.content_id.present?
22
+ end
23
+ end
24
+
5
25
  Attachment = Struct.new(:id, :part, :metadata, keyword_init: true) do
6
26
  def content_type
7
27
  metadata['content_type']
@@ -35,6 +55,14 @@ module MailDude
35
55
  end
36
56
  end
37
57
 
58
+ def attachments_count
59
+ attachment_parts.length
60
+ end
61
+
62
+ def attachments_present?
63
+ attachments_count.positive?
64
+ end
65
+
38
66
  def find(attachment_id)
39
67
  raise AttachmentNotFoundError, 'Attachment not found' unless attachment_id.to_s.match?(/\Aa\d+\z/)
40
68
 
@@ -44,6 +72,8 @@ module MailDude
44
72
 
45
73
  def find_inline_by_cid(content_id)
46
74
  normalized = normalize_content_id(content_id)
75
+ return nil if normalized.blank?
76
+
47
77
  attachments.find { |attachment| attachment.metadata['content_id'] == normalized }
48
78
  end
49
79
 
@@ -64,19 +94,7 @@ module MailDude
64
94
  def attachment_parts
65
95
  return [] unless mail
66
96
 
67
- mail.all_parts.select { |part| attachment_part?(part) }
68
- end
69
-
70
- def attachment_part?(part)
71
- !part.multipart? && (part.attachment? || part.filename.present? || inline_part?(part))
72
- end
73
-
74
- def content_disposition(part)
75
- part.content_disposition.to_s.split(';').first.to_s.downcase
76
- end
77
-
78
- def inline_part?(part)
79
- content_disposition(part) == 'inline' && part.content_id.present?
97
+ mail.all_parts.select { |part| self.class.attachment_part?(part) }
80
98
  end
81
99
 
82
100
  def mail
@@ -96,8 +114,8 @@ module MailDude
96
114
  'filename' => sanitize_filename(part.filename, fallback: "attachment-#{id}"),
97
115
  'content_type' => part.mime_type.presence || 'application/octet-stream',
98
116
  'content_id' => normalize_content_id(part.content_id),
99
- 'disposition' => content_disposition(part).presence || 'attachment',
100
- 'inline' => content_disposition(part) == 'inline',
117
+ 'disposition' => self.class.content_disposition(part).presence || 'attachment',
118
+ 'inline' => self.class.inline_renderable_part?(part),
101
119
  'size_bytes' => decoded_size(part)
102
120
  }
103
121
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailDude
4
+ class AttachmentScrubber
5
+ RAW_SOURCE_OMITTED = "MailDude omitted raw message source because attachment capture is disabled.\n"
6
+
7
+ def initialize(mail)
8
+ @mail = mail
9
+ end
10
+
11
+ def raw_source
12
+ source = mail&.to_s
13
+ return RAW_SOURCE_OMITTED if source.blank?
14
+
15
+ sanitized = Mail.read_from_string(source)
16
+ remove_attachments!(sanitized)
17
+ sanitized.to_s
18
+ rescue StandardError
19
+ RAW_SOURCE_OMITTED
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :mail
25
+
26
+ def remove_attachments!(message)
27
+ return remove_single_part_attachment!(message) unless message.multipart?
28
+
29
+ message.parts.recursive_delete_if { |part| AttachmentLocator.attachment_part?(part) }
30
+ end
31
+
32
+ def remove_single_part_attachment!(message)
33
+ message.body = '' if AttachmentLocator.attachment_part?(message)
34
+ end
35
+ end
36
+ end
@@ -41,7 +41,7 @@ module MailDude
41
41
  sender: presenter.sender_summary,
42
42
  recipients: presenter.recipient_summary,
43
43
  captured_at: presenter.captured_at_label,
44
- attachments_count: presenter.attachments.length,
44
+ attachments_count: presenter.attachment_count,
45
45
  attachment_count_label: presenter.attachment_count_label,
46
46
  mailer_label: presenter.mailer_label
47
47
  }
@@ -80,12 +80,12 @@ module MailDude
80
80
  raw_source.split(/\r?\n\r?\n/, 2).first.to_s
81
81
  end
82
82
 
83
- def has_attachments?
84
- attachments.any?
85
- end
83
+ def has_attachments? = metadata_value('has_attachments') == true || attachments.any?
84
+
85
+ def attachment_count = metadata_value('attachments_count').presence&.to_i || attachments.length
86
86
 
87
87
  def attachment_count_label
88
- count = attachments.length
88
+ count = attachment_count
89
89
  "#{count} #{'attachment'.pluralize(count)}"
90
90
  end
91
91
 
@@ -35,8 +35,8 @@ module MailDude
35
35
  'mailer_action' => internal_header(INTERNAL_ACTION_HEADER),
36
36
  'has_html' => part_present?('text/html'),
37
37
  'has_text' => part_present?('text/plain'),
38
- 'has_attachments' => attachments.any?,
39
- 'attachments_count' => attachments.length,
38
+ 'has_attachments' => attachment_locator.attachments_present?,
39
+ 'attachments_count' => attachment_locator.attachments_count,
40
40
  'attachments' => attachments,
41
41
  'size_bytes' => raw_source.bytesize
42
42
  }
@@ -54,8 +54,16 @@ module MailDude
54
54
  fallback.present? ? [fallback] : []
55
55
  end
56
56
 
57
- def attachments
58
- @attachments ||= AttachmentLocator.new(mail).attachments.map(&:metadata)
57
+ def attachments = @attachments ||= attachment_locator.attachments.map(&:metadata)
58
+
59
+ def attachment_locator
60
+ @attachment_locator ||= AttachmentLocator.new(attachment_source)
61
+ end
62
+
63
+ def attachment_source
64
+ return mail unless MailDude.configuration.capture_attachments && raw_source.present?
65
+
66
+ MessageRecord.new(id: id, metadata: {}, raw_source: raw_source)
59
67
  end
60
68
 
61
69
  def content_type
@@ -35,11 +35,17 @@ module MailDude
35
35
  private
36
36
 
37
37
  def build_record(mail, id: generate_id, captured_at: Time.now.utc)
38
- raw_source = mail.to_s
38
+ raw_source = raw_source_for(mail)
39
39
  metadata = MessageSerializer.new(mail, id: id, captured_at: captured_at, raw_source: raw_source).metadata
40
40
  MessageRecord.new(id: id, metadata: metadata, raw_source: raw_source)
41
41
  end
42
42
 
43
+ def raw_source_for(mail)
44
+ return mail.to_s if MailDude.configuration.capture_attachments
45
+
46
+ AttachmentScrubber.new(mail).raw_source
47
+ end
48
+
43
49
  def generate_id
44
50
  time = Time.now.utc
45
51
  "#{time.strftime('%Y%m%dT%H%M%S')}#{format('%06d', time.usec)}Z-#{SecureRandom.hex(8)}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MailDude
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
data/lib/mail_dude.rb CHANGED
@@ -17,6 +17,7 @@ require_relative 'mail_dude/pagination'
17
17
  require_relative 'mail_dude/message_serializer'
18
18
  require_relative 'mail_dude/message_presenter'
19
19
  require_relative 'mail_dude/attachment_locator'
20
+ require_relative 'mail_dude/attachment_scrubber'
20
21
  require_relative 'mail_dude/html_body_renderer'
21
22
  require_relative 'mail_dude/message_broadcast'
22
23
  require_relative 'mail_dude/stores/base'
metadata CHANGED
@@ -1,10 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mail_dude
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - MailDude contributors
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
11
  date: 2026-06-23 00:00:00.000000000 Z
@@ -184,6 +185,7 @@ files:
184
185
  - lib/generators/mail_dude/templates/initializer.tt
185
186
  - lib/mail_dude.rb
186
187
  - lib/mail_dude/attachment_locator.rb
188
+ - lib/mail_dude/attachment_scrubber.rb
187
189
  - lib/mail_dude/configuration.rb
188
190
  - lib/mail_dude/dashboard.rb
189
191
  - lib/mail_dude/delivery_method.rb
@@ -207,6 +209,7 @@ licenses:
207
209
  - MIT
208
210
  metadata:
209
211
  rubygems_mfa_required: 'true'
212
+ post_install_message:
210
213
  rdoc_options: []
211
214
  require_paths:
212
215
  - lib
@@ -221,7 +224,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
224
  - !ruby/object:Gem::Version
222
225
  version: '0'
223
226
  requirements: []
224
- rubygems_version: 3.6.2
227
+ rubygems_version: 3.3.7
228
+ signing_key:
225
229
  specification_version: 4
226
230
  summary: A Rails Action Mailer capture engine for development and QA.
227
231
  test_files: []