pec_ruby 0.1.0 → 0.2.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: 81169a18f557a07493e4dc4dd5d1daee96638209bcd95cca4e79d3d8b1e6fe70
4
- data.tar.gz: d84b84c4e2f75fcad73bbf8987dfe83ba9ff7d2997e5bda9a50117977b5096c4
3
+ metadata.gz: a2065977ec4bb7287a8fb74b7a24a2f02a5f8491e0e9c7ee5900583430643324
4
+ data.tar.gz: 8b1c48a688a30b6a717526b00ccb605274e0c27587f81615c48e529496fc3349
5
5
  SHA512:
6
- metadata.gz: b3eed5f566866056fa6fe9ac1bb5aa4d6698cd16ae6f56534ac8f63bdc307df5aa948f521765061d90963ba3284ec9d7583218fb9e5d2814a790fa18c97a8603
7
- data.tar.gz: 44a1840dbb4cd1feb585cab2ca2e2ca81b403b5154dca1408553cb9001790877d445460b2a1df7c4286bb1f373197a4a508f2d3970c3ef9aa2c63e223f9567d6
6
+ metadata.gz: 401cef0a64fed51a473b4c0626007daaf699cb35ae9fdd244280bea5faccef08550f80600fa9dc53c9def9c469e4b08b105634e3aca277196bde8a53cbf228c4
7
+ data.tar.gz: 7aa36dddfb5e5d354d682069eed41ffd5a3de9d6242e86f33856c6e58a98f45c6cb8405e3d1df5249625d229e93db2f0bca0d0fc7b7a5de7aecc968ea19e0c99
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rspec_status ADDED
@@ -0,0 +1,99 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------------- | ------ | --------------- |
3
+ ./spec/pec_ruby/attachment_spec.rb[1:1:1] | passed | 0.00043 seconds |
4
+ ./spec/pec_ruby/attachment_spec.rb[1:2:1:1] | passed | 0.00521 seconds |
5
+ ./spec/pec_ruby/attachment_spec.rb[1:2:2:1] | passed | 0.00009 seconds |
6
+ ./spec/pec_ruby/attachment_spec.rb[1:3:1:1] | passed | 0.00008 seconds |
7
+ ./spec/pec_ruby/attachment_spec.rb[1:3:2:1] | passed | 0.00007 seconds |
8
+ ./spec/pec_ruby/attachment_spec.rb[1:4:1] | passed | 0.00007 seconds |
9
+ ./spec/pec_ruby/attachment_spec.rb[1:5:1] | passed | 0.00007 seconds |
10
+ ./spec/pec_ruby/attachment_spec.rb[1:6:1] | passed | 0.00008 seconds |
11
+ ./spec/pec_ruby/attachment_spec.rb[1:7:1] | passed | 0.00033 seconds |
12
+ ./spec/pec_ruby/attachment_spec.rb[1:8:1] | passed | 0.0023 seconds |
13
+ ./spec/pec_ruby/attachment_spec.rb[1:8:2] | passed | 0.00063 seconds |
14
+ ./spec/pec_ruby/attachment_spec.rb[1:9:1] | passed | 0.00056 seconds |
15
+ ./spec/pec_ruby/attachment_spec.rb[1:10:1] | passed | 0.00051 seconds |
16
+ ./spec/pec_ruby/attachment_spec.rb[1:11:1] | passed | 0.00014 seconds |
17
+ ./spec/pec_ruby/attachment_spec.rb[1:12:1:1] | passed | 0.00055 seconds |
18
+ ./spec/pec_ruby/attachment_spec.rb[1:12:2:1] | passed | 0.00013 seconds |
19
+ ./spec/pec_ruby/attachment_spec.rb[1:12:3:1] | passed | 0.0001 seconds |
20
+ ./spec/pec_ruby/attachment_spec.rb[1:13:1:1] | passed | 0.00076 seconds |
21
+ ./spec/pec_ruby/attachment_spec.rb[1:13:2:1] | passed | 0.0002 seconds |
22
+ ./spec/pec_ruby/attachment_spec.rb[1:13:3:1] | passed | 0.00076 seconds |
23
+ ./spec/pec_ruby/attachment_spec.rb[2:1:1] | passed | 0.00004 seconds |
24
+ ./spec/pec_ruby/attachment_spec.rb[2:2:1] | passed | 0.00007 seconds |
25
+ ./spec/pec_ruby/attachment_spec.rb[2:3:1] | passed | 0.00007 seconds |
26
+ ./spec/pec_ruby/attachment_spec.rb[2:4:1] | passed | 0.00007 seconds |
27
+ ./spec/pec_ruby/attachment_spec.rb[2:5:1] | passed | 0.00008 seconds |
28
+ ./spec/pec_ruby/attachment_spec.rb[2:6:1:1] | passed | 0.00007 seconds |
29
+ ./spec/pec_ruby/attachment_spec.rb[2:6:2:1] | passed | 0.00124 seconds |
30
+ ./spec/pec_ruby/attachment_spec.rb[2:7:1] | passed | 0.00022 seconds |
31
+ ./spec/pec_ruby/attachment_spec.rb[2:8:1:1] | passed | 0.00008 seconds |
32
+ ./spec/pec_ruby/attachment_spec.rb[2:8:2:1] | passed | 0.00008 seconds |
33
+ ./spec/pec_ruby/client_spec.rb[1:1:1] | passed | 0.00004 seconds |
34
+ ./spec/pec_ruby/client_spec.rb[1:1:2] | passed | 0.00003 seconds |
35
+ ./spec/pec_ruby/client_spec.rb[1:1:3] | passed | 0.00028 seconds |
36
+ ./spec/pec_ruby/client_spec.rb[1:2:1:1] | passed | 0.00003 seconds |
37
+ ./spec/pec_ruby/client_spec.rb[1:2:2:1] | passed | 0.00008 seconds |
38
+ ./spec/pec_ruby/client_spec.rb[1:2:3:1] | passed | 0.00007 seconds |
39
+ ./spec/pec_ruby/client_spec.rb[1:3:1] | passed | 0.00021 seconds |
40
+ ./spec/pec_ruby/client_spec.rb[1:3:2] | passed | 0.00036 seconds |
41
+ ./spec/pec_ruby/client_spec.rb[1:3:3] | passed | 0.00028 seconds |
42
+ ./spec/pec_ruby/client_spec.rb[1:3:4] | passed | 0.00018 seconds |
43
+ ./spec/pec_ruby/client_spec.rb[1:3:5:1] | passed | 0.0002 seconds |
44
+ ./spec/pec_ruby/client_spec.rb[1:3:6:1] | passed | 0.0003 seconds |
45
+ ./spec/pec_ruby/client_spec.rb[1:4:1:1] | passed | 0.00015 seconds |
46
+ ./spec/pec_ruby/client_spec.rb[1:4:1:2] | passed | 0.00013 seconds |
47
+ ./spec/pec_ruby/client_spec.rb[1:4:2:1] | passed | 0.00004 seconds |
48
+ ./spec/pec_ruby/client_spec.rb[1:4:3:1] | passed | 0.00015 seconds |
49
+ ./spec/pec_ruby/client_spec.rb[1:5:1:1] | passed | 0.0001 seconds |
50
+ ./spec/pec_ruby/client_spec.rb[1:5:2:1] | passed | 0.00032 seconds |
51
+ ./spec/pec_ruby/client_spec.rb[1:5:2:2] | passed | 0.00022 seconds |
52
+ ./spec/pec_ruby/client_spec.rb[1:5:2:3] | passed | 0.00021 seconds |
53
+ ./spec/pec_ruby/client_spec.rb[1:5:2:4] | passed | 0.00108 seconds |
54
+ ./spec/pec_ruby/client_spec.rb[1:6:1:1] | passed | 0.00013 seconds |
55
+ ./spec/pec_ruby/client_spec.rb[1:6:2:1] | passed | 0.00181 seconds |
56
+ ./spec/pec_ruby/client_spec.rb[1:6:2:2] | passed | 0.00269 seconds |
57
+ ./spec/pec_ruby/client_spec.rb[1:6:3:1] | passed | 0.00018 seconds |
58
+ ./spec/pec_ruby/message_spec.rb[1:1:1] | passed | 0.0001 seconds |
59
+ ./spec/pec_ruby/message_spec.rb[1:2:1:1] | passed | 0.0001 seconds |
60
+ ./spec/pec_ruby/message_spec.rb[1:2:2:1] | passed | 0.00029 seconds |
61
+ ./spec/pec_ruby/message_spec.rb[1:2:2:2:1] | passed | 0.00016 seconds |
62
+ ./spec/pec_ruby/message_spec.rb[1:3:1:1] | passed | 0.00009 seconds |
63
+ ./spec/pec_ruby/message_spec.rb[1:3:2:1] | passed | 0.00025 seconds |
64
+ ./spec/pec_ruby/message_spec.rb[1:3:2:2:1] | passed | 0.00012 seconds |
65
+ ./spec/pec_ruby/message_spec.rb[1:4:1:1] | passed | 0.00009 seconds |
66
+ ./spec/pec_ruby/message_spec.rb[1:4:2:1] | passed | 0.00013 seconds |
67
+ ./spec/pec_ruby/message_spec.rb[1:5:1:1] | passed | 0.00018 seconds |
68
+ ./spec/pec_ruby/message_spec.rb[1:5:2:1] | passed | 0.00029 seconds |
69
+ ./spec/pec_ruby/message_spec.rb[1:6:1:1] | passed | 0.00011 seconds |
70
+ ./spec/pec_ruby/message_spec.rb[1:6:2:1] | passed | 0.0001 seconds |
71
+ ./spec/pec_ruby/message_spec.rb[1:7:1:1] | passed | 0.00019 seconds |
72
+ ./spec/pec_ruby/message_spec.rb[1:7:1:2] | passed | 0.00013 seconds |
73
+ ./spec/pec_ruby/message_spec.rb[1:7:2:1] | passed | 0.00026 seconds |
74
+ ./spec/pec_ruby/message_spec.rb[1:7:2:2] | passed | 0.00024 seconds |
75
+ ./spec/pec_ruby/message_spec.rb[1:7:3:1] | passed | 0.00017 seconds |
76
+ ./spec/pec_ruby/message_spec.rb[1:8:1:1] | passed | 0.00011 seconds |
77
+ ./spec/pec_ruby/message_spec.rb[1:8:2:1] | passed | 0.00012 seconds |
78
+ ./spec/pec_ruby/message_spec.rb[1:9:1:1] | passed | 0.0001 seconds |
79
+ ./spec/pec_ruby/message_spec.rb[1:9:2:1] | passed | 0.00017 seconds |
80
+ ./spec/pec_ruby/message_spec.rb[1:10:1:1] | passed | 0.0001 seconds |
81
+ ./spec/pec_ruby/message_spec.rb[1:10:2:1] | passed | 0.00129 seconds |
82
+ ./spec/pec_ruby/message_spec.rb[1:10:3:1] | passed | 0.00108 seconds |
83
+ ./spec/pec_ruby/message_spec.rb[1:11:1:1] | passed | 0.00026 seconds |
84
+ ./spec/pec_ruby/message_spec.rb[1:11:2:1] | passed | 0.00015 seconds |
85
+ ./spec/pec_ruby/message_spec.rb[1:12:1:1] | passed | 0.00024 seconds |
86
+ ./spec/pec_ruby/message_spec.rb[1:12:2:1] | passed | 0.00014 seconds |
87
+ ./spec/pec_ruby/message_spec.rb[1:13:1] | passed | 0.00016 seconds |
88
+ ./spec/pec_ruby/message_spec.rb[1:14:1] | passed | 0.00015 seconds |
89
+ ./spec/pec_ruby/message_spec.rb[1:15:1:1] | passed | 0.0001 seconds |
90
+ ./spec/pec_ruby/message_spec.rb[1:15:2:1] | passed | 0.0001 seconds |
91
+ ./spec/pec_ruby/message_spec.rb[1:16:1] | passed | 0.00015 seconds |
92
+ ./spec/pec_ruby/message_spec.rb[1:17:1] | passed | 0.00018 seconds |
93
+ ./spec/pec_ruby/message_spec.rb[1:18:1] | passed | 0.00057 seconds |
94
+ ./spec/pec_ruby_spec.rb[1:1] | passed | 0.00004 seconds |
95
+ ./spec/pec_ruby_spec.rb[1:2:1] | passed | 0.00005 seconds |
96
+ ./spec/pec_ruby_spec.rb[1:2:2] | passed | 0.00003 seconds |
97
+ ./spec/pec_ruby_spec.rb[1:2:3] | passed | 0.00003 seconds |
98
+ ./spec/pec_ruby_spec.rb[1:2:4] | passed | 0.00002 seconds |
99
+ ./spec/pec_ruby_spec.rb[1:2:5] | passed | 0.00002 seconds |
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2025-07-13
11
+
12
+ ### Changed
13
+ - **Breaking Change**: `original_body` now returns a hash with format information instead of plain text
14
+ - Hash contains: `content`, `content_type`, and `charset` keys
15
+ - Allows proper handling of HTML vs plain text content
16
+ - Preserves original formatting for correct display
17
+
18
+ ### Added
19
+ - `original_body_text` method for getting plain text content only
20
+ - `original_body_html` method for getting HTML content only
21
+ - Enhanced body format detection and handling
22
+ - Better charset handling for international content
23
+ - **Nested postacert.eml support** for handling forwarded PECs
24
+ - `Attachment#postacert?` to detect postacert.eml attachments
25
+ - `Attachment#as_postacert_message` to parse nested PECs
26
+ - `Message#nested_postacerts` to get nested postacert attachments
27
+ - `Message#original_regular_attachments` to get non-postacert attachments
28
+ - `Message#has_nested_postacerts?` to check for nested PECs
29
+ - `Message#nested_postacert_messages` to get parsed nested messages
30
+ - `Message#all_postacert_messages` for hierarchical view of all messages
31
+ - `PecRuby::NestedPostacertMessage` class for nested postacert handling
32
+ - Full API compatibility with original message methods
33
+ - Support for multi-level nesting (postacert within postacert)
34
+ - Automatic detection and parsing of deeper nesting levels
35
+
10
36
  ## [0.1.0] - 2025-07-13
11
37
 
12
38
  ### Added
@@ -33,5 +59,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
59
  - Aruba PEC (imaps.pec.aruba.it)
34
60
  - Generic IMAP-compliant PEC providers
35
61
 
36
- [Unreleased]: https://github.com/egio12/pec_ruby/compare/v0.1.0...HEAD
62
+ [Unreleased]: https://github.com/egio12/pec_ruby/compare/v0.2.0...HEAD
63
+ [0.2.0]: https://github.com/egio12/pec_ruby/compare/v0.1.0...v0.2.0
37
64
  [0.1.0]: https://github.com/egio12/pec_ruby/releases/tag/v0.1.0
data/Gemfile.lock ADDED
@@ -0,0 +1,84 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pec_ruby (0.2.0)
5
+ mail (~> 2.7)
6
+ net-imap (~> 0.3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.3)
12
+ date (3.4.1)
13
+ diff-lcs (1.6.2)
14
+ json (2.12.2)
15
+ language_server-protocol (3.17.0.5)
16
+ lint_roller (1.1.0)
17
+ mail (2.8.1)
18
+ mini_mime (>= 0.1.1)
19
+ net-imap
20
+ net-pop
21
+ net-smtp
22
+ mini_mime (1.1.5)
23
+ net-imap (0.5.9)
24
+ date
25
+ net-protocol
26
+ net-pop (0.1.2)
27
+ net-protocol
28
+ net-protocol (0.2.2)
29
+ timeout
30
+ net-smtp (0.5.1)
31
+ net-protocol
32
+ parallel (1.27.0)
33
+ parser (3.3.8.0)
34
+ ast (~> 2.4.1)
35
+ racc
36
+ prism (1.4.0)
37
+ racc (1.8.1)
38
+ rainbow (3.1.1)
39
+ rake (13.3.0)
40
+ regexp_parser (2.10.0)
41
+ rspec (3.13.1)
42
+ rspec-core (~> 3.13.0)
43
+ rspec-expectations (~> 3.13.0)
44
+ rspec-mocks (~> 3.13.0)
45
+ rspec-core (3.13.5)
46
+ rspec-support (~> 3.13.0)
47
+ rspec-expectations (3.13.5)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.13.0)
50
+ rspec-mocks (3.13.5)
51
+ diff-lcs (>= 1.2.0, < 2.0)
52
+ rspec-support (~> 3.13.0)
53
+ rspec-support (3.13.4)
54
+ rubocop (1.78.0)
55
+ json (~> 2.3)
56
+ language_server-protocol (~> 3.17.0.2)
57
+ lint_roller (~> 1.1.0)
58
+ parallel (~> 1.10)
59
+ parser (>= 3.3.0.2)
60
+ rainbow (>= 2.2.2, < 4.0)
61
+ regexp_parser (>= 2.9.3, < 3.0)
62
+ rubocop-ast (>= 1.45.1, < 2.0)
63
+ ruby-progressbar (~> 1.7)
64
+ unicode-display_width (>= 2.4.0, < 4.0)
65
+ rubocop-ast (1.45.1)
66
+ parser (>= 3.3.7.2)
67
+ prism (~> 1.4)
68
+ ruby-progressbar (1.13.0)
69
+ timeout (0.4.3)
70
+ unicode-display_width (3.1.4)
71
+ unicode-emoji (~> 4.0, >= 4.0.4)
72
+ unicode-emoji (4.0.4)
73
+
74
+ PLATFORMS
75
+ arm64-darwin-24
76
+
77
+ DEPENDENCIES
78
+ pec_ruby!
79
+ rake (~> 13.0)
80
+ rspec (~> 3.0)
81
+ rubocop (~> 1.21)
82
+
83
+ BUNDLED WITH
84
+ 2.4.19
data/README.md CHANGED
@@ -101,16 +101,33 @@ puts message.date # PEC message date
101
101
  # Original message information
102
102
  puts message.original_subject # Original subject
103
103
  puts message.original_from # Original sender
104
- puts message.original_body # Original message body
104
+ body_info = message.original_body # Original message body with format info
105
105
 
106
106
  # Attachments
107
107
  message.original_attachments.each do |attachment|
108
108
  puts "#{attachment.filename} (#{attachment.size_kb} KB)"
109
109
 
110
- # Save attachment
111
- attachment.save_to("/path/to/file.pdf")
112
- # or
113
- attachment.save_to_dir("/downloads/")
110
+ # Check if attachment is a nested postacert.eml (forwarded PEC)
111
+ if attachment.postacert?
112
+ puts " -> This is a nested postacert.eml!"
113
+ nested_msg = attachment.as_postacert_message
114
+ puts " -> Original subject: #{nested_msg.subject}"
115
+ puts " -> Original from: #{nested_msg.from}"
116
+ else
117
+ # Save regular attachment
118
+ attachment.save_to("/path/to/file.pdf")
119
+ # or
120
+ attachment.save_to_dir("/downloads/")
121
+ end
122
+ end
123
+
124
+ # Handle nested postacerts (forwarded PECs)
125
+ if message.has_nested_postacerts?
126
+ puts "This message contains #{message.nested_postacerts.size} forwarded PEC(s)"
127
+
128
+ message.nested_postacert_messages.each do |nested_msg|
129
+ puts "Nested PEC: #{nested_msg.subject} from #{nested_msg.from}"
130
+ end
114
131
  end
115
132
  ```
116
133
 
@@ -215,14 +232,52 @@ message.original_subject # String: Original subject
215
232
  message.original_from # String: Original sender
216
233
  message.original_to # Array<String>: Original recipients
217
234
  message.original_date # Time: Original message date
218
- message.original_body # String: Original message body (decoded)
235
+ message.original_body # Hash: Original message body with format info
236
+ message.original_body_text # String: Plain text body only
237
+ message.original_body_html # String: HTML body only
238
+ ```
239
+
240
+ ##### Original Message Body
241
+
242
+ The `original_body` method returns a hash with format information, allowing you to handle different content types appropriately:
243
+
244
+ ```ruby
245
+ body_info = message.original_body
246
+ if body_info
247
+ puts "Content type: #{body_info[:content_type]}"
248
+ puts "Charset: #{body_info[:charset]}"
249
+
250
+ case body_info[:content_type]
251
+ when 'text/html'
252
+ # Handle HTML content - preserve formatting for web display
253
+ html_content = body_info[:content]
254
+ # You can now render this in a web browser or HTML viewer
255
+ when 'text/plain'
256
+ # Handle plain text content
257
+ text_content = body_info[:content]
258
+ puts text_content
259
+ end
260
+ end
261
+
262
+ # Or use convenience methods for specific formats
263
+ text_only = message.original_body_text # Returns nil if no text/plain part
264
+ html_only = message.original_body_html # Returns nil if no text/html part
219
265
  ```
220
266
 
221
267
  ##### Attachments
222
268
 
223
269
  ```ruby
224
270
  # Get original message attachments
225
- message.original_attachments # Array<PecRuby::Attachment>
271
+ message.original_attachments # Array<PecRuby::Attachment> - All attachments
272
+ message.original_regular_attachments # Array<PecRuby::Attachment> - Non-postacert attachments only
273
+ message.nested_postacerts # Array<PecRuby::Attachment> - Nested postacert.eml files only
274
+
275
+ # Check for nested postacerts (forwarded PECs)
276
+ message.has_nested_postacerts? # Boolean
277
+ message.nested_postacert_messages # Array<PecRuby::NestedPostacertMessage>
278
+
279
+ # Get all postacert messages in a flattened structure
280
+ message.all_postacert_messages # Array<Hash> - Hierarchical view of all messages
226
281
  ```
227
282
 
228
283
  ##### Summary Information
@@ -254,11 +309,42 @@ attachment.content # String: Raw binary content
254
309
  attachment.save_to(path) # Save to specific path
255
310
  attachment.save_to_dir(directory) # Save to directory with original filename
256
311
 
312
+ # Nested postacert detection and parsing
313
+ attachment.postacert? # Boolean: Check if this is a postacert.eml
314
+ attachment.as_postacert_message # PecRuby::NestedPostacertMessage: Parse as nested PEC
315
+
257
316
  # Summary
258
317
  attachment.summary # Hash: Complete attachment information
259
318
  attachment.to_s # String: Human-readable description
260
319
  ```
261
320
 
321
+ ### PecRuby::NestedPostacertMessage
322
+
323
+ Represents a nested postacert.eml file (forwarded PEC) found within attachments.
324
+
325
+ #### Instance Methods
326
+
327
+ ```ruby
328
+ # Basic message information
329
+ nested_msg.subject # String: Subject of the nested message
330
+ nested_msg.from # String: Sender of the nested message
331
+ nested_msg.to # Array<String>: Recipients of the nested message
332
+ nested_msg.date # Time: Date of the nested message
333
+
334
+ # Body content (same API as original_body)
335
+ nested_msg.body # Hash: Body with content_type and charset info
336
+ nested_msg.body_text # String: Plain text body only
337
+ nested_msg.body_html # String: HTML body only
338
+
339
+ # Nested attachments
340
+ nested_msg.attachments # Array<PecRuby::Attachment>
341
+ nested_msg.nested_postacerts # Array<PecRuby::Attachment> - Even deeper nesting!
342
+ nested_msg.has_nested_postacerts? # Boolean: Check for deeper nesting
343
+
344
+ # Summary
345
+ nested_msg.summary # Hash: Complete nested message information
346
+ ```
347
+
262
348
  ## Complete Example
263
349
 
264
350
  ```ruby
@@ -279,14 +365,56 @@ begin
279
365
  pec_messages.each do |message|
280
366
  puts "Subject: #{message.original_subject}"
281
367
  puts "From: #{message.original_from}"
282
- puts "Attachments: #{message.original_attachments.size}"
368
+ puts "Total attachments: #{message.original_attachments.size}"
369
+ puts "Regular attachments: #{message.original_regular_attachments.size}"
370
+ puts "Nested PECs: #{message.nested_postacerts.size}"
371
+
372
+ # Handle message body based on format
373
+ body_info = message.original_body
374
+ if body_info
375
+ puts "Body format: #{body_info[:content_type]}"
376
+ case body_info[:content_type]
377
+ when 'text/html'
378
+ puts "HTML content available for web display"
379
+ # Save HTML to file for viewing
380
+ File.write("./downloads/message_#{message.uid}.html", body_info[:content])
381
+ when 'text/plain'
382
+ puts "Text content:"
383
+ puts body_info[:content][0..100] + "..." # First 100 chars
384
+ end
385
+ end
283
386
 
284
- # Download attachments
285
- message.original_attachments.each do |attachment|
387
+ # Download regular attachments
388
+ message.original_regular_attachments.each do |attachment|
286
389
  attachment.save_to_dir('./downloads')
287
390
  puts "Downloaded: #{attachment.filename}"
288
391
  end
289
392
 
393
+ # Handle nested postacerts (forwarded PECs)
394
+ if message.has_nested_postacerts?
395
+ puts "Found #{message.nested_postacerts.size} forwarded PEC(s):"
396
+
397
+ message.nested_postacert_messages.each_with_index do |nested_msg, index|
398
+ puts " Nested PEC ##{index + 1}:"
399
+ puts " Subject: #{nested_msg.subject}"
400
+ puts " From: #{nested_msg.from}"
401
+ puts " Attachments: #{nested_msg.attachments.size}"
402
+
403
+ # Download nested PEC attachments
404
+ nested_msg.attachments.each do |nested_attachment|
405
+ unless nested_attachment.postacert? # Avoid infinite recursion
406
+ nested_attachment.save_to_dir('./downloads/nested')
407
+ puts " Downloaded nested: #{nested_attachment.filename}"
408
+ end
409
+ end
410
+
411
+ # Check for even deeper nesting
412
+ if nested_msg.has_nested_postacerts?
413
+ puts " -> This nested PEC contains #{nested_msg.nested_postacerts.size} more nested PEC(s)!"
414
+ end
415
+ end
416
+ end
417
+
290
418
  puts "─" * 40
291
419
  end
292
420
 
@@ -356,9 +484,3 @@ bundle exec rubocop # Check code style
356
484
  ## License
357
485
 
358
486
  Distributed under the MIT License. See `LICENSE` for more information.
359
-
360
- ## Contact
361
-
362
- Enrico Giordano - enricomaria.giordano@icloud.com
363
-
364
- Project Link: [https://github.com/egio12/pec_ruby](https://github.com/egio12/pec_ruby)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'mail'
4
+
3
5
  module PecRuby
4
6
  class Attachment
5
7
  attr_reader :mail_attachment
@@ -57,5 +59,142 @@ module PecRuby
57
59
  def to_s
58
60
  "#{filename} (#{mime_type}, #{size_kb} KB)"
59
61
  end
62
+
63
+ # Check if this attachment is a postacert.eml file
64
+ def postacert?
65
+ filename&.downcase&.include?('postacert.eml') ||
66
+ (filename&.downcase&.end_with?('.eml') && mime_type&.include?('message'))
67
+ end
68
+
69
+ # Parse this attachment as a postacert.eml if it is one
70
+ # Returns a PecRuby::Message-like object for the nested postacert
71
+ def as_postacert_message
72
+ return nil unless postacert?
73
+
74
+ begin
75
+ # Parse the attachment content as an email message
76
+ nested_mail = Mail.read_from_string(content)
77
+
78
+ # Create a simplified message object for the nested postacert
79
+ NestedPostacertMessage.new(nested_mail)
80
+ rescue => e
81
+ raise PecRuby::Error, "Failed to parse nested postacert.eml: #{e.message}"
82
+ end
83
+ end
84
+ end
85
+
86
+ # Simplified message class for nested postacert emails
87
+ class NestedPostacertMessage
88
+ attr_reader :mail
89
+
90
+ def initialize(mail)
91
+ @mail = mail
92
+ end
93
+
94
+ def subject
95
+ @mail.subject
96
+ end
97
+
98
+ def from
99
+ @mail.from&.first
100
+ end
101
+
102
+ def to
103
+ @mail.to || []
104
+ end
105
+
106
+ def date
107
+ @mail.date
108
+ end
109
+
110
+ def body
111
+ # Try to get text/plain first, then text/html
112
+ text_part = extract_text_part(@mail, "text/plain")
113
+ html_part = extract_text_part(@mail, "text/html")
114
+ selected_part = text_part || html_part
115
+
116
+ return nil unless selected_part
117
+
118
+ raw_body = selected_part.body.decoded
119
+ charset = selected_part.charset ||
120
+ selected_part.content_type_parameters&.[]("charset") ||
121
+ "UTF-8"
122
+
123
+ content = raw_body.dup.force_encoding(charset).encode("UTF-8")
124
+
125
+ {
126
+ content: content,
127
+ content_type: selected_part.mime_type,
128
+ charset: charset
129
+ }
130
+ end
131
+
132
+ def body_text
133
+ text_part = extract_text_part(@mail, "text/plain")
134
+ return nil unless text_part
135
+
136
+ raw_body = text_part.body.decoded
137
+ charset = text_part.charset ||
138
+ text_part.content_type_parameters&.[]("charset") ||
139
+ "UTF-8"
140
+
141
+ raw_body.dup.force_encoding(charset).encode("UTF-8")
142
+ end
143
+
144
+ def body_html
145
+ html_part = extract_text_part(@mail, "text/html")
146
+ return nil unless html_part
147
+
148
+ raw_body = html_part.body.decoded
149
+ charset = html_part.charset ||
150
+ html_part.content_type_parameters&.[]("charset") ||
151
+ "UTF-8"
152
+
153
+ raw_body.dup.force_encoding(charset).encode("UTF-8")
154
+ end
155
+
156
+ def attachments
157
+ return [] unless @mail&.attachments
158
+
159
+ @mail.attachments.map { |att| Attachment.new(att) }
160
+ end
161
+
162
+ def summary
163
+ {
164
+ subject: subject,
165
+ from: from,
166
+ to: to,
167
+ date: date,
168
+ attachments_count: attachments.size,
169
+ nested_postacerts_count: nested_postacerts.size
170
+ }
171
+ end
172
+
173
+ # Find any nested postacert.eml files in this message's attachments
174
+ def nested_postacerts
175
+ attachments.select(&:postacert?)
176
+ end
177
+
178
+ # Check if this nested message has any nested postacert.eml files
179
+ def has_nested_postacerts?
180
+ !nested_postacerts.empty?
181
+ end
182
+
183
+ private
184
+
185
+ def extract_text_part(mail, preferred_type = "text/plain")
186
+ return mail unless mail.multipart?
187
+
188
+ mail.parts.each do |part|
189
+ if part.multipart?
190
+ found = extract_text_part(part, preferred_type)
191
+ return found if found
192
+ elsif part.mime_type == preferred_type
193
+ return part
194
+ end
195
+ end
196
+
197
+ nil
198
+ end
60
199
  end
61
200
  end
@@ -43,7 +43,8 @@ module PecRuby
43
43
  end
44
44
 
45
45
  def connected?
46
- @imap && !@imap.disconnected?
46
+ return false unless @imap
47
+ !@imap.disconnected?
47
48
  end
48
49
 
49
50
  # Get all messages or a subset
@@ -20,7 +20,7 @@ module PecRuby
20
20
  return nil unless @envelope.subject
21
21
 
22
22
  decoded = Mail::Encodings.value_decode(@envelope.subject)
23
- decoded.gsub!("POSTA CERTIFICATA:", "") if decoded.start_with?("POSTA CERTIFICATA:")
23
+ decoded = decoded.gsub("POSTA CERTIFICATA:", "") if decoded.start_with?("POSTA CERTIFICATA:")
24
24
  decoded.strip
25
25
  end
26
26
 
@@ -89,13 +89,15 @@ module PecRuby
89
89
  postacert_message&.date
90
90
  end
91
91
 
92
- # Get original message body (text/plain preferred)
92
+ # Get original message body with format information
93
93
  def original_body
94
94
  mail = postacert_message
95
95
  return nil unless mail
96
96
 
97
97
  text_part = extract_text_part(mail, "text/plain")
98
98
  html_part = extract_text_part(mail, "text/html")
99
+
100
+ # Prefer text/plain, but return HTML if that's all we have
99
101
  selected_part = text_part || html_part
100
102
 
101
103
  return nil unless selected_part
@@ -105,7 +107,45 @@ module PecRuby
105
107
  selected_part.content_type_parameters&.[]("charset") ||
106
108
  "UTF-8"
107
109
 
108
- raw_body.force_encoding(charset).encode("UTF-8")
110
+ content = raw_body.dup.force_encoding(charset).encode("UTF-8")
111
+
112
+ {
113
+ content: content,
114
+ content_type: selected_part.mime_type,
115
+ charset: charset
116
+ }
117
+ end
118
+
119
+ # Get original message body as plain text only
120
+ def original_body_text
121
+ mail = postacert_message
122
+ return nil unless mail
123
+
124
+ text_part = extract_text_part(mail, "text/plain")
125
+ return nil unless text_part
126
+
127
+ raw_body = text_part.body.decoded
128
+ charset = text_part.charset ||
129
+ text_part.content_type_parameters&.[]("charset") ||
130
+ "UTF-8"
131
+
132
+ raw_body.dup.force_encoding(charset).encode("UTF-8")
133
+ end
134
+
135
+ # Get original message body as HTML only
136
+ def original_body_html
137
+ mail = postacert_message
138
+ return nil unless mail
139
+
140
+ html_part = extract_text_part(mail, "text/html")
141
+ return nil unless html_part
142
+
143
+ raw_body = html_part.body.decoded
144
+ charset = html_part.charset ||
145
+ html_part.content_type_parameters&.[]("charset") ||
146
+ "UTF-8"
147
+
148
+ raw_body.dup.force_encoding(charset).encode("UTF-8")
109
149
  end
110
150
 
111
151
  # Get original message attachments
@@ -116,6 +156,69 @@ module PecRuby
116
156
  mail.attachments.map { |att| Attachment.new(att) }
117
157
  end
118
158
 
159
+ # Get original message attachments that are NOT postacert.eml files
160
+ def original_regular_attachments
161
+ original_attachments.reject(&:postacert?)
162
+ end
163
+
164
+ # Get nested postacert.eml files from original message attachments
165
+ def nested_postacerts
166
+ original_attachments.select(&:postacert?)
167
+ end
168
+
169
+ # Check if original message has nested postacert.eml files
170
+ def has_nested_postacerts?
171
+ !nested_postacerts.empty?
172
+ end
173
+
174
+ # Get all nested postacert messages parsed and ready to use
175
+ def nested_postacert_messages
176
+ nested_postacerts.map(&:as_postacert_message).compact
177
+ end
178
+
179
+ # Get a flattened view of all postacert messages (original + nested)
180
+ # Returns array with the original message first, followed by nested ones
181
+ def all_postacert_messages
182
+ messages = []
183
+
184
+ # Add the main postacert message (this message)
185
+ if has_postacert?
186
+ messages << {
187
+ level: 0,
188
+ message: self,
189
+ type: :main_postacert
190
+ }
191
+ end
192
+
193
+ # Add nested postacert messages
194
+ nested_postacert_messages.each_with_index do |nested_msg, index|
195
+ messages << {
196
+ level: 1,
197
+ message: nested_msg,
198
+ type: :nested_postacert,
199
+ index: index
200
+ }
201
+
202
+ # Check for deeper nesting (postacert within postacert within postacert)
203
+ if nested_msg.has_nested_postacerts?
204
+ nested_msg.nested_postacerts.each_with_index do |deep_nested, deep_index|
205
+ deep_nested_msg = deep_nested.as_postacert_message
206
+ if deep_nested_msg
207
+ messages << {
208
+ level: 2,
209
+ message: deep_nested_msg,
210
+ type: :deep_nested_postacert,
211
+ parent_index: index,
212
+ index: deep_index
213
+ }
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ messages
220
+ end
221
+
119
222
  # Summary information
120
223
  def summary
121
224
  {
@@ -129,7 +232,11 @@ module PecRuby
129
232
  original_from: original_from,
130
233
  original_to: original_to,
131
234
  original_date: original_date,
132
- attachments_count: original_attachments.size
235
+ attachments_count: original_attachments.size,
236
+ regular_attachments_count: original_regular_attachments.size,
237
+ nested_postacerts_count: nested_postacerts.size,
238
+ has_nested_postacerts: has_nested_postacerts?,
239
+ total_postacert_messages: all_postacert_messages.size
133
240
  }
134
241
  end
135
242
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PecRuby
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/pec_ruby.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/pec_ruby/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pec_ruby"
7
+ spec.version = PecRuby::VERSION
8
+ spec.authors = ["EMG"]
9
+ spec.email = ["enricomaria.giordano@icloud.com"]
10
+
11
+ spec.summary = "Ruby gem for decoding and reading Italian PEC (Posta Elettronica Certificata) emails"
12
+ spec.description = "A comprehensive Ruby library for handling Italian certified email (PEC) messages. Includes methods for extracting postacert.eml contents, decoding attachments, and a CLI for exploring PEC messages."
13
+ spec.homepage = "https://github.com/egio12/pec_ruby"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/egio12/pec_ruby"
20
+ spec.metadata["changelog_uri"] = "https://github.com/egio12/pec_ruby/blob/main/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z 2>/dev/null`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "bin"
29
+ spec.executables = ["pec_ruby"]
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Core dependencies
33
+ spec.add_dependency "mail", "~> 2.7"
34
+ spec.add_dependency "net-imap", "~> 0.3"
35
+
36
+ # Development dependencies
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "rubocop", "~> 1.0"
39
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pec_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - EMG
@@ -76,10 +76,14 @@ executables:
76
76
  extensions: []
77
77
  extra_rdoc_files: []
78
78
  files:
79
+ - ".rspec"
80
+ - ".rspec_status"
79
81
  - CHANGELOG.md
80
82
  - Gemfile
83
+ - Gemfile.lock
81
84
  - LICENSE
82
85
  - README.md
86
+ - Rakefile
83
87
  - bin/pec_ruby
84
88
  - lib/pec_ruby.rb
85
89
  - lib/pec_ruby/attachment.rb
@@ -87,6 +91,7 @@ files:
87
91
  - lib/pec_ruby/client.rb
88
92
  - lib/pec_ruby/message.rb
89
93
  - lib/pec_ruby/version.rb
94
+ - pec_ruby.gemspec
90
95
  homepage: https://github.com/egio12/pec_ruby
91
96
  licenses:
92
97
  - MIT