pec_ruby 0.1.0 → 0.2.1

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: 88d9db11466fbbf9fff86f31304a5bb4192ee83768657010df58285591556f8d
4
+ data.tar.gz: 7c3d1dfdee2ec73bce2b8a47974aa89cb3e68ef5e173225ee1ddae6c48dd02c3
5
5
  SHA512:
6
- metadata.gz: b3eed5f566866056fa6fe9ac1bb5aa4d6698cd16ae6f56534ac8f63bdc307df5aa948f521765061d90963ba3284ec9d7583218fb9e5d2814a790fa18c97a8603
7
- data.tar.gz: 44a1840dbb4cd1feb585cab2ca2e2ca81b403b5154dca1408553cb9001790877d445460b2a1df7c4286bb1f373197a4a508f2d3970c3ef9aa2c63e223f9567d6
6
+ metadata.gz: c549c540649866f806a5a48a877ee54c3707a39561fa751562cd5c6d155265b5ad50aeb0f76569356d6cccd44d078fa51a53d2087d98192e2ae9d9216f429e46
7
+ data.tar.gz: 90b7400f64ff7ff65721c120900b52abe3f2d7aba08ce8398ba9c49fac0e371b79a23bddd9939589a4f53f336041982902fb4cbe3bded882daaea7458274d133
data/.env.example ADDED
@@ -0,0 +1,10 @@
1
+ # PEC Configuration
2
+ # Copy this file to .env and fill in your actual values
3
+
4
+ # IMAP Server Configuration
5
+ PEC_HOST=imaps.pec.aruba.it
6
+ PEC_USERNAME=your@domain.pec.it
7
+ PEC_PASSWORD=your_password
8
+
9
+ # Test Configuration (optional)
10
+ PEC_TEST_UID=1234
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rspec_status ADDED
@@ -0,0 +1,118 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------------------- | ------ | --------------- |
3
+ ./spec/pec_ruby/attachment_spec.rb[1:1:1] | passed | 0.00045 seconds |
4
+ ./spec/pec_ruby/attachment_spec.rb[1:2:1:1] | passed | 0.00446 seconds |
5
+ ./spec/pec_ruby/attachment_spec.rb[1:2:2:1] | passed | 0.00011 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.00029 seconds |
12
+ ./spec/pec_ruby/attachment_spec.rb[1:8:1] | passed | 0.00074 seconds |
13
+ ./spec/pec_ruby/attachment_spec.rb[1:8:2] | passed | 0.00036 seconds |
14
+ ./spec/pec_ruby/attachment_spec.rb[1:9:1] | passed | 0.00076 seconds |
15
+ ./spec/pec_ruby/attachment_spec.rb[1:10:1] | passed | 0.00065 seconds |
16
+ ./spec/pec_ruby/attachment_spec.rb[1:11:1] | passed | 0.00016 seconds |
17
+ ./spec/pec_ruby/attachment_spec.rb[1:12:1:1] | passed | 0.00045 seconds |
18
+ ./spec/pec_ruby/attachment_spec.rb[1:12:2:1] | passed | 0.00012 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.00073 seconds |
21
+ ./spec/pec_ruby/attachment_spec.rb[1:13:2:1] | passed | 0.00019 seconds |
22
+ ./spec/pec_ruby/attachment_spec.rb[1:13:3:1] | passed | 0.0008 seconds |
23
+ ./spec/pec_ruby/attachment_spec.rb[2:1:1] | passed | 0.00005 seconds |
24
+ ./spec/pec_ruby/attachment_spec.rb[2:2:1] | passed | 0.00008 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.00015 seconds |
27
+ ./spec/pec_ruby/attachment_spec.rb[2:5:1] | passed | 0.00007 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.00016 seconds |
30
+ ./spec/pec_ruby/attachment_spec.rb[2:7:1] | passed | 0.0002 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.00012 seconds |
33
+ ./spec/pec_ruby/client_spec.rb[1:1:1] | passed | 0.00005 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.00003 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.00014 seconds |
38
+ ./spec/pec_ruby/client_spec.rb[1:2:3:1] | passed | 0.0003 seconds |
39
+ ./spec/pec_ruby/client_spec.rb[1:3:1] | passed | 0.00049 seconds |
40
+ ./spec/pec_ruby/client_spec.rb[1:3:2] | passed | 0.00021 seconds |
41
+ ./spec/pec_ruby/client_spec.rb[1:3:3] | passed | 0.00019 seconds |
42
+ ./spec/pec_ruby/client_spec.rb[1:3:4] | passed | 0.00015 seconds |
43
+ ./spec/pec_ruby/client_spec.rb[1:3:5:1] | passed | 0.00019 seconds |
44
+ ./spec/pec_ruby/client_spec.rb[1:3:6:1] | passed | 0.00018 seconds |
45
+ ./spec/pec_ruby/client_spec.rb[1:4:1:1] | passed | 0.00018 seconds |
46
+ ./spec/pec_ruby/client_spec.rb[1:4:1:2] | passed | 0.00014 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.00049 seconds |
50
+ ./spec/pec_ruby/client_spec.rb[1:5:2:1] | passed | 0.00024 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.00035 seconds |
53
+ ./spec/pec_ruby/client_spec.rb[1:5:2:4] | passed | 0.00021 seconds |
54
+ ./spec/pec_ruby/client_spec.rb[1:6:1:1] | passed | 0.00011 seconds |
55
+ ./spec/pec_ruby/client_spec.rb[1:6:2:1] | passed | 0.00018 seconds |
56
+ ./spec/pec_ruby/client_spec.rb[1:6:2:2] | passed | 0.00035 seconds |
57
+ ./spec/pec_ruby/client_spec.rb[1:6:3:1] | passed | 0.00014 seconds |
58
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:1:1] | failed | 0.0025 seconds |
59
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:1:2] | failed | 0.00085 seconds |
60
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:1:3] | failed | 0.00088 seconds |
61
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:2:1] | failed | 0.00086 seconds |
62
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:3:1] | failed | 0.00089 seconds |
63
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:3:2] | failed | 0.00073 seconds |
64
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:4:1] | failed | 0.00081 seconds |
65
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:4:2] | failed | 0.00068 seconds |
66
+ ./spec/pec_ruby/message_refactoring_spec.rb[1:5:1] | failed | 0.00058 seconds |
67
+ ./spec/pec_ruby/message_spec.rb[1:1:1] | passed | 0.00024 seconds |
68
+ ./spec/pec_ruby/message_spec.rb[1:2:1:1] | passed | 0.00046 seconds |
69
+ ./spec/pec_ruby/message_spec.rb[1:2:2:1] | passed | 0.00021 seconds |
70
+ ./spec/pec_ruby/message_spec.rb[1:2:2:2:1] | passed | 0.00029 seconds |
71
+ ./spec/pec_ruby/message_spec.rb[1:3:1:1] | passed | 0.00012 seconds |
72
+ ./spec/pec_ruby/message_spec.rb[1:3:2:1] | passed | 0.00013 seconds |
73
+ ./spec/pec_ruby/message_spec.rb[1:3:2:2:1] | passed | 0.00013 seconds |
74
+ ./spec/pec_ruby/message_spec.rb[1:4:1:1] | passed | 0.00019 seconds |
75
+ ./spec/pec_ruby/message_spec.rb[1:4:2:1] | passed | 0.00013 seconds |
76
+ ./spec/pec_ruby/message_spec.rb[1:5:1:1] | passed | 0.0001 seconds |
77
+ ./spec/pec_ruby/message_spec.rb[1:5:2:1] | passed | 0.0014 seconds |
78
+ ./spec/pec_ruby/message_spec.rb[1:6:1:1] | passed | 0.0002 seconds |
79
+ ./spec/pec_ruby/message_spec.rb[1:6:2:1] | passed | 0.00012 seconds |
80
+ ./spec/pec_ruby/message_spec.rb[1:7:1:1] | passed | 0.00013 seconds |
81
+ ./spec/pec_ruby/message_spec.rb[1:7:1:2] | passed | 0.00015 seconds |
82
+ ./spec/pec_ruby/message_spec.rb[1:7:2:1] | passed | 0.00029 seconds |
83
+ ./spec/pec_ruby/message_spec.rb[1:7:2:2] | passed | 0.00132 seconds |
84
+ ./spec/pec_ruby/message_spec.rb[1:7:3:1] | passed | 0.00024 seconds |
85
+ ./spec/pec_ruby/message_spec.rb[1:8:1:1] | passed | 0.00012 seconds |
86
+ ./spec/pec_ruby/message_spec.rb[1:8:2:1] | passed | 0.0001 seconds |
87
+ ./spec/pec_ruby/message_spec.rb[1:9:1:1] | passed | 0.00013 seconds |
88
+ ./spec/pec_ruby/message_spec.rb[1:9:2:1] | passed | 0.00018 seconds |
89
+ ./spec/pec_ruby/message_spec.rb[1:10:1:1] | passed | 0.00015 seconds |
90
+ ./spec/pec_ruby/message_spec.rb[1:10:2:1] | passed | 0.00298 seconds |
91
+ ./spec/pec_ruby/message_spec.rb[1:10:3:1] | passed | 0.0018 seconds |
92
+ ./spec/pec_ruby/message_spec.rb[1:11:1:1] | passed | 0.00038 seconds |
93
+ ./spec/pec_ruby/message_spec.rb[1:11:2:1] | passed | 0.00018 seconds |
94
+ ./spec/pec_ruby/message_spec.rb[1:12:1:1] | passed | 0.00042 seconds |
95
+ ./spec/pec_ruby/message_spec.rb[1:12:2:1] | passed | 0.00016 seconds |
96
+ ./spec/pec_ruby/message_spec.rb[1:13:1] | passed | 0.00023 seconds |
97
+ ./spec/pec_ruby/message_spec.rb[1:14:1] | passed | 0.00026 seconds |
98
+ ./spec/pec_ruby/message_spec.rb[1:15:1:1] | passed | 0.00011 seconds |
99
+ ./spec/pec_ruby/message_spec.rb[1:15:2:1] | passed | 0.0001 seconds |
100
+ ./spec/pec_ruby/message_spec.rb[1:16:1] | passed | 0.00017 seconds |
101
+ ./spec/pec_ruby/message_spec.rb[1:17:1] | passed | 0.00023 seconds |
102
+ ./spec/pec_ruby/message_spec.rb[1:18:1] | passed | 0.00619 seconds |
103
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:1:1] | failed | 0.00106 seconds |
104
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:1:2] | failed | 0.00146 seconds |
105
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:1:3] | failed | 0.00163 seconds |
106
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:1:4] | failed | 0.00081 seconds |
107
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:1:5] | failed | 0.00136 seconds |
108
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:1:6] | failed | 0.00155 seconds |
109
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:2:1] | failed | 0.00058 seconds |
110
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:2:2] | failed | 0.00052 seconds |
111
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:3:1] | failed | 0.00056 seconds |
112
+ ./spec/pec_ruby/nested_postacert_spec.rb[1:3:2] | failed | 0.00062 seconds |
113
+ ./spec/pec_ruby_spec.rb[1:1] | passed | 0.00006 seconds |
114
+ ./spec/pec_ruby_spec.rb[1:2:1] | passed | 0.00005 seconds |
115
+ ./spec/pec_ruby_spec.rb[1:2:2] | passed | 0.00003 seconds |
116
+ ./spec/pec_ruby_spec.rb[1:2:3] | passed | 0.00004 seconds |
117
+ ./spec/pec_ruby_spec.rb[1:2:4] | passed | 0.00005 seconds |
118
+ ./spec/pec_ruby_spec.rb[1:2:5] | passed | 0.00019 seconds |
data/CHANGELOG.md CHANGED
@@ -5,7 +5,56 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.2.1] - 2025-07-13
9
+
10
+ ### Fixed
11
+ - **Fixed forwarded postacert.eml detection**: Now correctly detects and processes postacert.eml files that are forwarded as attachments in nested MESSAGE/RFC822 structures
12
+ - Improved nested postacert part ID detection algorithm to handle complex IMAP message structures
13
+
14
+ ### Added
15
+ - Comprehensive test suite for nested postacert detection scenarios
16
+ - Performance optimizations with memoization for attachment calculations
17
+ - Enhanced error handling for malformed IMAP structures
18
+ - Support for environment variable configuration (`PEC_HOST`, `PEC_USERNAME`, `PEC_PASSWORD`)
19
+ - Security improvements to prevent credential leaks in test files
20
+
21
+ ### Changed
22
+ - **Code refactoring**: Unified duplicate postacert detection methods into single parameterized method
23
+ - **Performance**: Added memoization to `original_attachments`, `nested_postacerts`, and `nested_postacert_attachments` methods
24
+ - **Security**: Test files now use environment variables instead of hardcoded credentials
25
+ - Improved robustness of bodystructure parsing with defensive programming techniques
26
+
27
+ ### Security
28
+ - Removed all hardcoded credentials from test files
29
+ - Added `.env.example` for secure configuration
30
+ - Enhanced `.gitignore` to prevent accidental credential commits
31
+ - Added security documentation in README
32
+
33
+ ## [0.2.0] - 2025-07-13
34
+
35
+ ### Changed
36
+ - **Breaking Change**: `original_body` now returns a hash with format information instead of plain text
37
+ - Hash contains: `content`, `content_type`, and `charset` keys
38
+ - Allows proper handling of HTML vs plain text content
39
+ - Preserves original formatting for correct display
40
+
41
+ ### Added
42
+ - `original_body_text` method for getting plain text content only
43
+ - `original_body_html` method for getting HTML content only
44
+ - Enhanced body format detection and handling
45
+ - Better charset handling for international content
46
+ - **Nested postacert.eml support** for handling forwarded PECs
47
+ - `Attachment#postacert?` to detect postacert.eml attachments
48
+ - `Attachment#as_postacert_message` to parse nested PECs
49
+ - `Message#nested_postacerts` to get nested postacert attachments
50
+ - `Message#original_regular_attachments` to get non-postacert attachments
51
+ - `Message#has_nested_postacerts?` to check for nested PECs
52
+ - `Message#nested_postacert_messages` to get parsed nested messages
53
+ - `Message#all_postacert_messages` for hierarchical view of all messages
54
+ - `PecRuby::NestedPostacertMessage` class for nested postacert handling
55
+ - Full API compatibility with original message methods
56
+ - Support for multi-level nesting (postacert within postacert)
57
+ - Automatic detection and parsing of deeper nesting levels
9
58
 
10
59
  ## [0.1.0] - 2025-07-13
11
60
 
@@ -33,5 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
82
  - Aruba PEC (imaps.pec.aruba.it)
34
83
  - Generic IMAP-compliant PEC providers
35
84
 
36
- [Unreleased]: https://github.com/egio12/pec_ruby/compare/v0.1.0...HEAD
85
+ [Unreleased]: https://github.com/egio12/pec_ruby/compare/v0.2.1...HEAD
86
+ [0.2.1]: https://github.com/egio12/pec_ruby/compare/v0.2.0...v0.2.1
87
+ [0.2.0]: https://github.com/egio12/pec_ruby/compare/v0.1.0...v0.2.0
37
88
  [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.1)
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
@@ -6,9 +6,12 @@ A comprehensive Ruby gem for decoding and managing Italian PEC (Posta Elettronic
6
6
 
7
7
  - **IMAP Connection**: Connect to Italian PEC servers
8
8
  - **Automatic Extraction**: Automatically extracts original messages from postacert.eml attachments
9
+ - **Nested PEC Support**: **NEW in v0.2.1** - Detects and processes forwarded PEC messages (nested postacert.eml files)
9
10
  - **Attachment Management**: Download and manage attachments easily
11
+ - **Performance Optimized**: **NEW in v0.2.1** - Memoization for faster repeated access to attachments
10
12
  - **CLI Included**: Command-line interface for exploring PEC messages
11
13
  - **Programmatic API**: Methods for integrating PEC functionality into your Ruby applications
14
+ - **Comprehensive Testing**: Full test suite with both unit and integration tests
12
15
 
13
16
  ## Installation
14
17
 
@@ -57,6 +60,8 @@ The CLI allows you to:
57
60
  - Explore received messages
58
61
  - View decoded original message contents
59
62
  - Download attachments
63
+ - **NEW in v0.2.1**: Detect and process forwarded PEC messages (nested postacert.eml files)
64
+ - **NEW in v0.2.1**: Enhanced performance with memoization for large attachments
60
65
 
61
66
  ## Programmatic Usage
62
67
 
@@ -101,16 +106,33 @@ puts message.date # PEC message date
101
106
  # Original message information
102
107
  puts message.original_subject # Original subject
103
108
  puts message.original_from # Original sender
104
- puts message.original_body # Original message body
109
+ body_info = message.original_body # Original message body with format info
105
110
 
106
111
  # Attachments
107
112
  message.original_attachments.each do |attachment|
108
113
  puts "#{attachment.filename} (#{attachment.size_kb} KB)"
109
114
 
110
- # Save attachment
111
- attachment.save_to("/path/to/file.pdf")
112
- # or
113
- attachment.save_to_dir("/downloads/")
115
+ # Check if attachment is a nested postacert.eml (forwarded PEC)
116
+ if attachment.postacert?
117
+ puts " -> This is a nested postacert.eml!"
118
+ nested_msg = attachment.as_postacert_message
119
+ puts " -> Original subject: #{nested_msg.subject}"
120
+ puts " -> Original from: #{nested_msg.from}"
121
+ else
122
+ # Save regular attachment
123
+ attachment.save_to("/path/to/file.pdf")
124
+ # or
125
+ attachment.save_to_dir("/downloads/")
126
+ end
127
+ end
128
+
129
+ # Handle nested postacerts (forwarded PECs)
130
+ if message.has_nested_postacerts?
131
+ puts "This message contains #{message.nested_postacerts.size} forwarded PEC(s)"
132
+
133
+ message.nested_postacert_messages.each do |nested_msg|
134
+ puts "Nested PEC: #{nested_msg.subject} from #{nested_msg.from}"
135
+ end
114
136
  end
115
137
  ```
116
138
 
@@ -132,6 +154,15 @@ PecRuby::Client.new(host:, username:, password:, ssl: true)
132
154
  - `password` (String): Account password
133
155
  - `ssl` (Boolean): Use SSL connection (default: true)
134
156
 
157
+ > **Security Note**: For production usage, consider using environment variables instead of hardcoding credentials:
158
+ > ```ruby
159
+ > client = PecRuby::Client.new(
160
+ > host: ENV['PEC_HOST'],
161
+ > username: ENV['PEC_USERNAME'],
162
+ > password: ENV['PEC_PASSWORD']
163
+ > )
164
+ > ```
165
+
135
166
  #### Instance Methods
136
167
 
137
168
  ##### `#connect`
@@ -215,14 +246,52 @@ message.original_subject # String: Original subject
215
246
  message.original_from # String: Original sender
216
247
  message.original_to # Array<String>: Original recipients
217
248
  message.original_date # Time: Original message date
218
- message.original_body # String: Original message body (decoded)
249
+ message.original_body # Hash: Original message body with format info
250
+ message.original_body_text # String: Plain text body only
251
+ message.original_body_html # String: HTML body only
252
+ ```
253
+
254
+ ##### Original Message Body
255
+
256
+ The `original_body` method returns a hash with format information, allowing you to handle different content types appropriately:
257
+
258
+ ```ruby
259
+ body_info = message.original_body
260
+ if body_info
261
+ puts "Content type: #{body_info[:content_type]}"
262
+ puts "Charset: #{body_info[:charset]}"
263
+
264
+ case body_info[:content_type]
265
+ when 'text/html'
266
+ # Handle HTML content - preserve formatting for web display
267
+ html_content = body_info[:content]
268
+ # You can now render this in a web browser or HTML viewer
269
+ when 'text/plain'
270
+ # Handle plain text content
271
+ text_content = body_info[:content]
272
+ puts text_content
273
+ end
274
+ end
275
+
276
+ # Or use convenience methods for specific formats
277
+ text_only = message.original_body_text # Returns nil if no text/plain part
278
+ html_only = message.original_body_html # Returns nil if no text/html part
219
279
  ```
220
280
 
221
281
  ##### Attachments
222
282
 
223
283
  ```ruby
224
284
  # Get original message attachments
225
- message.original_attachments # Array<PecRuby::Attachment>
285
+ message.original_attachments # Array<PecRuby::Attachment> - All attachments
286
+ message.original_regular_attachments # Array<PecRuby::Attachment> - Non-postacert attachments only
287
+ message.nested_postacerts # Array<PecRuby::Attachment> - Nested postacert.eml files only
288
+
289
+ # Check for nested postacerts (forwarded PECs)
290
+ message.has_nested_postacerts? # Boolean
291
+ message.nested_postacert_messages # Array<PecRuby::NestedPostacertMessage>
292
+
293
+ # Get all postacert messages in a flattened structure
294
+ message.all_postacert_messages # Array<Hash> - Hierarchical view of all messages
226
295
  ```
227
296
 
228
297
  ##### Summary Information
@@ -254,11 +323,42 @@ attachment.content # String: Raw binary content
254
323
  attachment.save_to(path) # Save to specific path
255
324
  attachment.save_to_dir(directory) # Save to directory with original filename
256
325
 
326
+ # Nested postacert detection and parsing
327
+ attachment.postacert? # Boolean: Check if this is a postacert.eml
328
+ attachment.as_postacert_message # PecRuby::NestedPostacertMessage: Parse as nested PEC
329
+
257
330
  # Summary
258
331
  attachment.summary # Hash: Complete attachment information
259
332
  attachment.to_s # String: Human-readable description
260
333
  ```
261
334
 
335
+ ### PecRuby::NestedPostacertMessage
336
+
337
+ Represents a nested postacert.eml file (forwarded PEC) found within attachments.
338
+
339
+ #### Instance Methods
340
+
341
+ ```ruby
342
+ # Basic message information
343
+ nested_msg.subject # String: Subject of the nested message
344
+ nested_msg.from # String: Sender of the nested message
345
+ nested_msg.to # Array<String>: Recipients of the nested message
346
+ nested_msg.date # Time: Date of the nested message
347
+
348
+ # Body content (same API as original_body)
349
+ nested_msg.body # Hash: Body with content_type and charset info
350
+ nested_msg.body_text # String: Plain text body only
351
+ nested_msg.body_html # String: HTML body only
352
+
353
+ # Nested attachments
354
+ nested_msg.attachments # Array<PecRuby::Attachment>
355
+ nested_msg.nested_postacerts # Array<PecRuby::Attachment> - Even deeper nesting!
356
+ nested_msg.has_nested_postacerts? # Boolean: Check for deeper nesting
357
+
358
+ # Summary
359
+ nested_msg.summary # Hash: Complete nested message information
360
+ ```
361
+
262
362
  ## Complete Example
263
363
 
264
364
  ```ruby
@@ -279,14 +379,56 @@ begin
279
379
  pec_messages.each do |message|
280
380
  puts "Subject: #{message.original_subject}"
281
381
  puts "From: #{message.original_from}"
282
- puts "Attachments: #{message.original_attachments.size}"
382
+ puts "Total attachments: #{message.original_attachments.size}"
383
+ puts "Regular attachments: #{message.original_regular_attachments.size}"
384
+ puts "Nested PECs: #{message.nested_postacerts.size}"
385
+
386
+ # Handle message body based on format
387
+ body_info = message.original_body
388
+ if body_info
389
+ puts "Body format: #{body_info[:content_type]}"
390
+ case body_info[:content_type]
391
+ when 'text/html'
392
+ puts "HTML content available for web display"
393
+ # Save HTML to file for viewing
394
+ File.write("./downloads/message_#{message.uid}.html", body_info[:content])
395
+ when 'text/plain'
396
+ puts "Text content:"
397
+ puts body_info[:content][0..100] + "..." # First 100 chars
398
+ end
399
+ end
283
400
 
284
- # Download attachments
285
- message.original_attachments.each do |attachment|
401
+ # Download regular attachments
402
+ message.original_regular_attachments.each do |attachment|
286
403
  attachment.save_to_dir('./downloads')
287
404
  puts "Downloaded: #{attachment.filename}"
288
405
  end
289
406
 
407
+ # Handle nested postacerts (forwarded PECs)
408
+ if message.has_nested_postacerts?
409
+ puts "Found #{message.nested_postacerts.size} forwarded PEC(s):"
410
+
411
+ message.nested_postacert_messages.each_with_index do |nested_msg, index|
412
+ puts " Nested PEC ##{index + 1}:"
413
+ puts " Subject: #{nested_msg.subject}"
414
+ puts " From: #{nested_msg.from}"
415
+ puts " Attachments: #{nested_msg.attachments.size}"
416
+
417
+ # Download nested PEC attachments
418
+ nested_msg.attachments.each do |nested_attachment|
419
+ unless nested_attachment.postacert? # Avoid infinite recursion
420
+ nested_attachment.save_to_dir('./downloads/nested')
421
+ puts " Downloaded nested: #{nested_attachment.filename}"
422
+ end
423
+ end
424
+
425
+ # Check for even deeper nesting
426
+ if nested_msg.has_nested_postacerts?
427
+ puts " -> This nested PEC contains #{nested_msg.nested_postacerts.size} more nested PEC(s)!"
428
+ end
429
+ end
430
+ end
431
+
290
432
  puts "─" * 40
291
433
  end
292
434
 
@@ -295,6 +437,41 @@ ensure
295
437
  end
296
438
  ```
297
439
 
440
+ ### Nested PEC Detection Example (NEW in v0.2.1)
441
+
442
+ Handle forwarded PEC messages that contain other PEC messages as attachments:
443
+
444
+ ```ruby
445
+ # Find a message with forwarded PECs
446
+ message = client.pec_messages.find { |msg| msg.has_nested_postacerts? }
447
+
448
+ if message
449
+ puts "Found message with #{message.nested_postacerts.size} forwarded PEC(s):"
450
+
451
+ # Process each forwarded PEC
452
+ message.nested_postacert_messages.each_with_index do |nested_msg, index|
453
+ puts " Forwarded PEC ##{index + 1}:"
454
+ puts " Subject: #{nested_msg.subject}"
455
+ puts " From: #{nested_msg.from}"
456
+ puts " Date: #{nested_msg.date}"
457
+ puts " Attachments: #{nested_msg.attachments.size}"
458
+
459
+ # Download attachments from the forwarded PEC
460
+ nested_msg.attachments.each do |attachment|
461
+ unless attachment.postacert? # Avoid infinite recursion
462
+ attachment.save_to_dir('./downloads/forwarded')
463
+ puts " Downloaded: #{attachment.filename}"
464
+ end
465
+ end
466
+
467
+ # Check for even deeper nesting (PEC forwarded within forwarded PEC)
468
+ if nested_msg.has_nested_postacerts?
469
+ puts " -> Contains #{nested_msg.nested_postacerts.size} more forwarded PEC(s)!"
470
+ end
471
+ end
472
+ end
473
+ ```
474
+
298
475
  ## Error Handling
299
476
 
300
477
  The gem defines several specific error classes:
@@ -335,6 +512,46 @@ Other providers should work if they support standard IMAP, but have not been tes
335
512
  - **Provider Testing**: Only tested with Aruba PEC. Other providers may work but are not guaranteed.
336
513
  - **Legal Compliance**: This library has not been evaluated for compliance with Italian PEC regulations or legal requirements. The message parsing methods used may not preserve all legally required aspects of certified email messages. Users should consult with legal experts and review applicable regulations before using this library in legally sensitive contexts.
337
514
 
515
+ ## Testing & Development Configuration
516
+
517
+ ### Environment Variables
518
+
519
+ For security, use environment variables to configure your PEC credentials:
520
+
521
+ ```bash
522
+ # Copy the example environment file
523
+ cp .env.example .env
524
+
525
+ # Edit .env with your actual credentials
526
+ export PEC_HOST=imaps.pec.aruba.it
527
+ export PEC_USERNAME=your@domain.pec.it
528
+ export PEC_PASSWORD=your_password
529
+ export PEC_TEST_UID=1234 # Optional: specific message UID for testing
530
+ ```
531
+
532
+ ### Running Tests
533
+
534
+ The gem includes comprehensive tests for all functionality:
535
+
536
+ ```bash
537
+ bundle install
538
+
539
+ # Run all tests (will skip integration tests without PEC credentials)
540
+ bundle exec rspec
541
+
542
+ # Run with PEC credentials for full integration testing
543
+ PEC_HOST=imaps.pec.aruba.it PEC_USERNAME=your@domain.pec.it PEC_PASSWORD=your_password bundle exec rspec
544
+
545
+ # Run specific test suites
546
+ bundle exec rspec spec/pec_ruby/nested_postacert_spec.rb # Nested PEC detection tests
547
+ bundle exec rspec spec/pec_ruby/message_refactoring_spec.rb # Performance & refactoring tests
548
+
549
+ # Check code style
550
+ bundle exec rubocop
551
+ ```
552
+
553
+ **Note**: Integration tests require real PEC credentials and will be skipped if environment variables are not set. Unit tests will always run.
554
+
338
555
  ## Development
339
556
 
340
557
  After cloning the repository:
@@ -356,9 +573,3 @@ bundle exec rubocop # Check code style
356
573
  ## License
357
574
 
358
575
  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,157 @@ 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
199
+ end
200
+
201
+ # Simple wrapper for mail attachments that mimics Mail::Attachment interface
202
+ class SimpleMailAttachment
203
+ attr_reader :mail, :filename, :mime_type
204
+
205
+ def initialize(mail, filename, mime_type)
206
+ @mail = mail
207
+ @filename = filename
208
+ @mime_type = mime_type
209
+ end
210
+
211
+ def decoded
212
+ @mail.to_s
213
+ end
60
214
  end
61
215
  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,15 +107,127 @@ 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
+ }
109
117
  end
110
118
 
111
- # Get original message attachments
112
- def original_attachments
119
+ # Get original message body as plain text only
120
+ def original_body_text
113
121
  mail = postacert_message
114
- return [] unless mail&.attachments
122
+ return nil unless mail
115
123
 
116
- mail.attachments.map { |att| Attachment.new(att) }
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")
149
+ end
150
+
151
+ # Get original message attachments (with memoization)
152
+ def original_attachments
153
+ @original_attachments ||= begin
154
+ mail = postacert_message
155
+ attachments = []
156
+
157
+ # Add attachments from the original message
158
+ if mail&.attachments
159
+ attachments += mail.attachments.map { |att| Attachment.new(att) }
160
+ end
161
+
162
+ # Also check for postacert.eml attachments in the outer message structure
163
+ # This handles cases where postacert.eml files are forwarded as attachments
164
+ attachments += nested_postacert_attachments
165
+
166
+ attachments
167
+ end
168
+ end
169
+
170
+ # Get original message attachments that are NOT postacert.eml files
171
+ def original_regular_attachments
172
+ original_attachments.reject(&:postacert?)
173
+ end
174
+
175
+ # Get nested postacert.eml files from original message attachments
176
+ def nested_postacerts
177
+ @nested_postacerts ||= original_attachments.select(&:postacert?)
178
+ end
179
+
180
+ # Check if original message has nested postacert.eml files
181
+ def has_nested_postacerts?
182
+ !nested_postacerts.empty?
183
+ end
184
+
185
+ # Get all nested postacert messages parsed and ready to use
186
+ def nested_postacert_messages
187
+ nested_postacerts.map(&:as_postacert_message).compact
188
+ end
189
+
190
+ # Get a flattened view of all postacert messages (original + nested)
191
+ # Returns array with the original message first, followed by nested ones
192
+ def all_postacert_messages
193
+ messages = []
194
+
195
+ # Add the main postacert message (this message)
196
+ if has_postacert?
197
+ messages << {
198
+ level: 0,
199
+ message: self,
200
+ type: :main_postacert
201
+ }
202
+ end
203
+
204
+ # Add nested postacert messages
205
+ nested_postacert_messages.each_with_index do |nested_msg, index|
206
+ messages << {
207
+ level: 1,
208
+ message: nested_msg,
209
+ type: :nested_postacert,
210
+ index: index
211
+ }
212
+
213
+ # Check for deeper nesting (postacert within postacert within postacert)
214
+ if nested_msg.has_nested_postacerts?
215
+ nested_msg.nested_postacerts.each_with_index do |deep_nested, deep_index|
216
+ deep_nested_msg = deep_nested.as_postacert_message
217
+ if deep_nested_msg
218
+ messages << {
219
+ level: 2,
220
+ message: deep_nested_msg,
221
+ type: :deep_nested_postacert,
222
+ parent_index: index,
223
+ index: deep_index
224
+ }
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ messages
117
231
  end
118
232
 
119
233
  # Summary information
@@ -129,7 +243,11 @@ module PecRuby
129
243
  original_from: original_from,
130
244
  original_to: original_to,
131
245
  original_date: original_date,
132
- attachments_count: original_attachments.size
246
+ attachments_count: original_attachments.size,
247
+ regular_attachments_count: original_regular_attachments.size,
248
+ nested_postacerts_count: nested_postacerts.size,
249
+ has_nested_postacerts: has_nested_postacerts?,
250
+ total_postacert_messages: all_postacert_messages.size
133
251
  }
134
252
  end
135
253
 
@@ -149,36 +267,67 @@ module PecRuby
149
267
  email
150
268
  end
151
269
 
152
- def find_postacert_part_ids(bodystructure = @bodystructure, path = "")
270
+ def nested_postacert_attachments
271
+ @nested_postacert_attachments ||= begin
272
+ attachments = []
273
+
274
+ # Find all postacert.eml parts (including nested)
275
+ all_postacert_part_ids = find_postacert_part_ids(@bodystructure, "", true)
276
+ main_postacert_part_id = find_postacert_part_ids.first
277
+
278
+ # Add any postacert.eml parts that are not the main one as attachments
279
+ nested_postacert_part_ids = all_postacert_part_ids - [main_postacert_part_id]
280
+
281
+ nested_postacert_part_ids.each do |part_id|
282
+ begin
283
+ raw_data = @client.fetch_body_part(@uid, part_id)
284
+ mail_data = Mail.read_from_string(raw_data)
285
+
286
+ # Create a simplified attachment wrapper
287
+ wrapper = SimpleMailAttachment.new(mail_data, "postacert.eml", "message/rfc822")
288
+ attachments << Attachment.new(wrapper)
289
+ rescue => e
290
+ puts "Warning: Failed to extract nested postacert.eml at #{part_id}: #{e.message}"
291
+ end
292
+ end
293
+
294
+ attachments
295
+ end
296
+ end
297
+
298
+
299
+ def find_postacert_part_ids(bodystructure = @bodystructure, path = "", include_nested = false)
153
300
  results = []
301
+
302
+ return results unless bodystructure
154
303
 
155
304
  if bodystructure.respond_to?(:parts) && bodystructure.parts
156
305
  bodystructure.parts.each_with_index do |part, index|
157
306
  part_path = path.empty? ? "#{index + 1}" : "#{path}.#{index + 1}"
158
- results += find_postacert_part_ids(part, part_path)
307
+ results += find_postacert_part_ids(part, part_path, include_nested)
159
308
  end
160
- elsif bodystructure.media_type == "MESSAGE" && bodystructure.subtype == "RFC822"
161
- if bodystructure.param && bodystructure.param["NAME"]&.downcase&.include?("postacert.eml")
309
+ elsif bodystructure.respond_to?(:media_type) && bodystructure.media_type == "MESSAGE" &&
310
+ bodystructure.respond_to?(:subtype) && bodystructure.subtype == "RFC822"
311
+ if bodystructure.respond_to?(:param) && bodystructure.param &&
312
+ bodystructure.param["NAME"]&.downcase&.include?("postacert.eml")
162
313
  results << path
163
314
  end
315
+
316
+ # Search inside MESSAGE/RFC822 bodies for nested postacert.eml files if requested
317
+ if include_nested && bodystructure.respond_to?(:body) && bodystructure.body&.respond_to?(:parts)
318
+ bodystructure.body.parts.each_with_index do |nested_part, nested_index|
319
+ nested_path = "#{path}.#{nested_index + 1}"
320
+ results += find_postacert_part_ids(nested_part, nested_path, include_nested)
321
+ end
322
+ end
164
323
  end
165
324
 
166
325
  results
167
326
  end
168
327
 
328
+ # Delegate to the shared implementation in NestedPostacertMessage
169
329
  def extract_text_part(mail, preferred_type = "text/plain")
170
- return mail unless mail.multipart?
171
-
172
- mail.parts.each do |part|
173
- if part.multipart?
174
- found = extract_text_part(part, preferred_type)
175
- return found if found
176
- elsif part.mime_type == preferred_type
177
- return part
178
- end
179
- end
180
-
181
- nil
330
+ NestedPostacertMessage.new(mail).send(:extract_text_part, mail, preferred_type)
182
331
  end
183
332
  end
184
333
  end
@@ -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.1"
5
5
  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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - EMG
@@ -67,8 +67,9 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
69
  description: A comprehensive Ruby library for handling Italian certified email (PEC)
70
- messages. Includes methods for extracting postacert.eml contents, decoding attachments,
71
- and a CLI for exploring PEC messages.
70
+ messages. Supports nested PEC detection, extracting postacert.eml contents, attachment
71
+ management, and includes a CLI for exploring PEC messages. Features performance
72
+ optimizations and secure credential handling.
72
73
  email:
73
74
  - enricomaria.giordano@icloud.com
74
75
  executables:
@@ -76,10 +77,15 @@ executables:
76
77
  extensions: []
77
78
  extra_rdoc_files: []
78
79
  files:
80
+ - ".env.example"
81
+ - ".rspec"
82
+ - ".rspec_status"
79
83
  - CHANGELOG.md
80
84
  - Gemfile
85
+ - Gemfile.lock
81
86
  - LICENSE
82
87
  - README.md
88
+ - Rakefile
83
89
  - bin/pec_ruby
84
90
  - lib/pec_ruby.rb
85
91
  - lib/pec_ruby/attachment.rb