mail_dude 0.1.1 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +1 -1
- data/lib/mail_dude/attachment_locator.rb +33 -15
- data/lib/mail_dude/attachment_scrubber.rb +36 -0
- data/lib/mail_dude/message_broadcast.rb +1 -1
- data/lib/mail_dude/message_presenter.rb +4 -4
- data/lib/mail_dude/message_serializer.rb +7 -3
- data/lib/mail_dude/stores/base.rb +7 -1
- data/lib/mail_dude/version.rb +1 -1
- data/lib/mail_dude.rb +1 -0
- metadata +5 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38404e9ab732d38243310741c269a4ec27e02a3076c479c891fe7bd532474d97
|
|
4
|
+
data.tar.gz: c66340b339905d47989e60b49d86073aab27785eae7a9da526fd910914db81ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d28003d59309eccded62419a0d28573facd4a027045c9df28eefa7491206f5815bc9906f60cb2631a8ada54921bbf59c9b3c418fa28a89462eca055946ee2ef
|
|
7
|
+
data.tar.gz: 7d8011434a33827a7f795f3bf865b7995dfec41d999aab8ef9cfd8d99e2f8b9d3ecf725c75f74deddbb4702fb9f8353666e66565254ca4e23c21f8e1a89b9be2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
- Fix CID image rendering for Content-ID attachments without inline disposition.
|
|
6
|
+
- Strip attachment payloads from stored raw source when attachment capture is disabled.
|
|
7
|
+
|
|
8
|
+
## 0.1.2
|
|
9
|
+
|
|
10
|
+
- Add official Ruby 3.1 support.
|
|
11
|
+
|
|
3
12
|
## 0.1.1
|
|
4
13
|
|
|
5
14
|
- Add compatibility with Rails >= 7.0.3.1.
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Local and QA applications often need realistic email delivery flows without risk
|
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
|
-
MailDude supports Ruby 3.
|
|
11
|
+
MailDude supports Ruby 3.1+ and Rails >= 7.0.3.1, < 8.0. CI covers Ruby 3.1 and 3.2 across Rails 7.0.3.1, 7.1, and 7.2.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
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' =>
|
|
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.
|
|
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
|
-
|
|
85
|
-
|
|
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 =
|
|
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' =>
|
|
39
|
-
'attachments_count' =>
|
|
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
|
}
|
|
@@ -55,7 +55,11 @@ module MailDude
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def attachments
|
|
58
|
-
@attachments ||=
|
|
58
|
+
@attachments ||= attachment_locator.attachments.map(&:metadata)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def attachment_locator
|
|
62
|
+
@attachment_locator ||= AttachmentLocator.new(mail)
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
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
|
|
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)}"
|
data/lib/mail_dude/version.rb
CHANGED
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,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mail_dude
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- MailDude contributors
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
10
|
+
date: 2026-06-23 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -185,6 +184,7 @@ files:
|
|
|
185
184
|
- lib/generators/mail_dude/templates/initializer.tt
|
|
186
185
|
- lib/mail_dude.rb
|
|
187
186
|
- lib/mail_dude/attachment_locator.rb
|
|
187
|
+
- lib/mail_dude/attachment_scrubber.rb
|
|
188
188
|
- lib/mail_dude/configuration.rb
|
|
189
189
|
- lib/mail_dude/dashboard.rb
|
|
190
190
|
- lib/mail_dude/delivery_method.rb
|
|
@@ -208,7 +208,6 @@ licenses:
|
|
|
208
208
|
- MIT
|
|
209
209
|
metadata:
|
|
210
210
|
rubygems_mfa_required: 'true'
|
|
211
|
-
post_install_message:
|
|
212
211
|
rdoc_options: []
|
|
213
212
|
require_paths:
|
|
214
213
|
- lib
|
|
@@ -216,15 +215,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
216
215
|
requirements:
|
|
217
216
|
- - ">="
|
|
218
217
|
- !ruby/object:Gem::Version
|
|
219
|
-
version: '3.
|
|
218
|
+
version: '3.1'
|
|
220
219
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
221
220
|
requirements:
|
|
222
221
|
- - ">="
|
|
223
222
|
- !ruby/object:Gem::Version
|
|
224
223
|
version: '0'
|
|
225
224
|
requirements: []
|
|
226
|
-
rubygems_version: 3.
|
|
227
|
-
signing_key:
|
|
225
|
+
rubygems_version: 3.6.2
|
|
228
226
|
specification_version: 4
|
|
229
227
|
summary: A Rails Action Mailer capture engine for development and QA.
|
|
230
228
|
test_files: []
|