pec_ruby 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 81169a18f557a07493e4dc4dd5d1daee96638209bcd95cca4e79d3d8b1e6fe70
4
+ data.tar.gz: d84b84c4e2f75fcad73bbf8987dfe83ba9ff7d2997e5bda9a50117977b5096c4
5
+ SHA512:
6
+ metadata.gz: b3eed5f566866056fa6fe9ac1bb5aa4d6698cd16ae6f56534ac8f63bdc307df5aa948f521765061d90963ba3284ec9d7583218fb9e5d2814a790fa18c97a8603
7
+ data.tar.gz: 44a1840dbb4cd1feb585cab2ca2e2ca81b403b5154dca1408553cb9001790877d445460b2a1df7c4286bb1f373197a4a508f2d3970c3ef9aa2c63e223f9567d6
data/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-07-13
11
+
12
+ ### Added
13
+ - Initial release of PecRuby gem
14
+ - IMAP client for connecting to Italian PEC servers
15
+ - Automatic extraction of postacert.eml attachments
16
+ - Message parsing and decoding functionality
17
+ - Attachment management with download capabilities
18
+ - Command-line interface (CLI) for interactive exploration
19
+ - Comprehensive error handling
20
+ - Support for flexible installation (with/without CLI dependencies)
21
+ - Complete API documentation
22
+ - Example usage files
23
+
24
+ ### Features
25
+ - **PecRuby::Client**: Main client class for IMAP operations
26
+ - **PecRuby::Message**: Message representation with original content extraction
27
+ - **PecRuby::Attachment**: Attachment handling with save capabilities
28
+ - **PecRuby::CLI**: Interactive command-line interface
29
+ - **Error Classes**: Specific error types for better error handling
30
+ - **Flexible Dependencies**: Optional CLI dependencies for minimal installations
31
+
32
+ ### Supported Providers
33
+ - Aruba PEC (imaps.pec.aruba.it)
34
+ - Generic IMAP-compliant PEC providers
35
+
36
+ [Unreleased]: https://github.com/egio12/pec_ruby/compare/v0.1.0...HEAD
37
+ [0.1.0]: https://github.com/egio12/pec_ruby/releases/tag/v0.1.0
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rubocop", "~> 1.21"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 EMG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,364 @@
1
+ # PecRuby
2
+
3
+ A comprehensive Ruby gem for decoding and managing Italian PEC (Posta Elettronica Certificata) email messages.
4
+
5
+ ## Features
6
+
7
+ - **IMAP Connection**: Connect to Italian PEC servers
8
+ - **Automatic Extraction**: Automatically extracts original messages from postacert.eml attachments
9
+ - **Attachment Management**: Download and manage attachments easily
10
+ - **CLI Included**: Command-line interface for exploring PEC messages
11
+ - **Programmatic API**: Methods for integrating PEC functionality into your Ruby applications
12
+
13
+ ## Installation
14
+
15
+ ### Library Only (without CLI)
16
+
17
+ To use only the programmatic API without the command-line interface:
18
+
19
+ ```ruby
20
+ gem 'pec_ruby'
21
+ ```
22
+
23
+ Or install directly:
24
+
25
+ ```bash
26
+ gem install pec_ruby
27
+ ```
28
+
29
+ ### With CLI Included
30
+
31
+ To also use the command-line interface, install additional dependencies:
32
+
33
+ ```bash
34
+ gem install pec_ruby tty-prompt awesome_print
35
+ ```
36
+
37
+ Or in your Gemfile:
38
+
39
+ ```ruby
40
+ gem 'pec_ruby'
41
+ gem 'tty-prompt', '~> 0.23'
42
+ gem 'awesome_print', '~> 1.9'
43
+ ```
44
+
45
+ ## CLI Usage
46
+
47
+ After complete installation (with CLI dependencies), you can use the CLI:
48
+
49
+ ```bash
50
+ pec_ruby
51
+ ```
52
+
53
+ **Note**: If you installed only the library without CLI dependencies, the `pec_ruby` executable will inform you how to install them.
54
+
55
+ The CLI allows you to:
56
+ - Connect to your PEC server
57
+ - Explore received messages
58
+ - View decoded original message contents
59
+ - Download attachments
60
+
61
+ ## Programmatic Usage
62
+
63
+ ### Basic Connection
64
+
65
+ ```ruby
66
+ require 'pec_ruby'
67
+
68
+ # Connect to PEC server
69
+ client = PecRuby::Client.new(
70
+ host: 'imaps.pec.aruba.it',
71
+ username: 'your@domain.pec.it',
72
+ password: 'password'
73
+ )
74
+
75
+ client.connect
76
+ ```
77
+
78
+ ### Retrieving Messages
79
+
80
+ ```ruby
81
+ # All messages (last 10)
82
+ messages = client.messages(limit: 10)
83
+
84
+ # Only PEC messages (with postacert.eml)
85
+ pec_messages = client.pec_messages(limit: 10)
86
+
87
+ # Specific message by UID
88
+ message = client.message(12345)
89
+ ```
90
+
91
+ ### Working with Messages
92
+
93
+ ```ruby
94
+ message = client.pec_messages.first
95
+
96
+ # PEC container information
97
+ puts message.subject # PEC message subject
98
+ puts message.from # PEC sender
99
+ puts message.date # PEC message date
100
+
101
+ # Original message information
102
+ puts message.original_subject # Original subject
103
+ puts message.original_from # Original sender
104
+ puts message.original_body # Original message body
105
+
106
+ # Attachments
107
+ message.original_attachments.each do |attachment|
108
+ puts "#{attachment.filename} (#{attachment.size_kb} KB)"
109
+
110
+ # Save attachment
111
+ attachment.save_to("/path/to/file.pdf")
112
+ # or
113
+ attachment.save_to_dir("/downloads/")
114
+ end
115
+ ```
116
+
117
+ ## API Documentation
118
+
119
+ ### PecRuby::Client
120
+
121
+ The main client class for connecting to PEC servers.
122
+
123
+ #### Constructor
124
+
125
+ ```ruby
126
+ PecRuby::Client.new(host:, username:, password:, ssl: true)
127
+ ```
128
+
129
+ **Parameters:**
130
+ - `host` (String): IMAP server hostname
131
+ - `username` (String): PEC email address
132
+ - `password` (String): Account password
133
+ - `ssl` (Boolean): Use SSL connection (default: true)
134
+
135
+ #### Instance Methods
136
+
137
+ ##### `#connect`
138
+ Establishes connection to the PEC server.
139
+
140
+ ```ruby
141
+ client.connect
142
+ # Returns: self
143
+ # Raises: PecRuby::ConnectionError, PecRuby::AuthenticationError
144
+ ```
145
+
146
+ ##### `#disconnect`
147
+ Safely disconnects from the PEC server.
148
+
149
+ ```ruby
150
+ client.disconnect
151
+ # Returns: nil
152
+ ```
153
+
154
+ ##### `#connected?`
155
+ Checks if currently connected to the server.
156
+
157
+ ```ruby
158
+ client.connected?
159
+ # Returns: Boolean
160
+ ```
161
+
162
+ ##### `#messages(limit: nil, reverse: true)`
163
+ Retrieves messages from the server.
164
+
165
+ ```ruby
166
+ messages = client.messages(limit: 10, reverse: true)
167
+ # Returns: Array<PecRuby::Message>
168
+ ```
169
+
170
+ **Parameters:**
171
+ - `limit` (Integer, optional): Maximum number of messages to retrieve
172
+ - `reverse` (Boolean): Return newest messages first (default: true)
173
+
174
+ ##### `#pec_messages(limit: nil, reverse: true)`
175
+ Retrieves only messages containing postacert.eml.
176
+
177
+ ```ruby
178
+ pec_messages = client.pec_messages(limit: 5)
179
+ # Returns: Array<PecRuby::Message>
180
+ ```
181
+
182
+ ##### `#message(uid)`
183
+ Retrieves a specific message by UID.
184
+
185
+ ```ruby
186
+ message = client.message(12345)
187
+ # Returns: PecRuby::Message or nil
188
+ ```
189
+
190
+ ### PecRuby::Message
191
+
192
+ Represents a PEC message with access to both container and original message data.
193
+
194
+ #### Instance Methods
195
+
196
+ ##### Basic PEC Container Information
197
+
198
+ ```ruby
199
+ # PEC envelope information
200
+ message.uid # Integer: Message UID
201
+ message.subject # String: PEC subject (cleaned)
202
+ message.from # String: PEC sender
203
+ message.to # Array<String>: PEC recipients
204
+ message.date # Time: PEC message date
205
+ ```
206
+
207
+ ##### Original Message Access
208
+
209
+ ```ruby
210
+ # Check if postacert.eml is available
211
+ message.has_postacert? # Boolean
212
+
213
+ # Original message information
214
+ message.original_subject # String: Original subject
215
+ message.original_from # String: Original sender
216
+ message.original_to # Array<String>: Original recipients
217
+ message.original_date # Time: Original message date
218
+ message.original_body # String: Original message body (decoded)
219
+ ```
220
+
221
+ ##### Attachments
222
+
223
+ ```ruby
224
+ # Get original message attachments
225
+ message.original_attachments # Array<PecRuby::Attachment>
226
+ ```
227
+
228
+ ##### Summary Information
229
+
230
+ ```ruby
231
+ # Get complete message summary
232
+ summary = message.summary
233
+ # Returns: Hash with all message information
234
+ ```
235
+
236
+ ### PecRuby::Attachment
237
+
238
+ Represents an attachment from the original message.
239
+
240
+ #### Instance Methods
241
+
242
+ ```ruby
243
+ # Basic information
244
+ attachment.filename # String: Original filename
245
+ attachment.mime_type # String: MIME type
246
+ attachment.size # Integer: Size in bytes
247
+ attachment.size_kb # Float: Size in KB
248
+ attachment.size_mb # Float: Size in MB
249
+
250
+ # Content access
251
+ attachment.content # String: Raw binary content
252
+
253
+ # File operations
254
+ attachment.save_to(path) # Save to specific path
255
+ attachment.save_to_dir(directory) # Save to directory with original filename
256
+
257
+ # Summary
258
+ attachment.summary # Hash: Complete attachment information
259
+ attachment.to_s # String: Human-readable description
260
+ ```
261
+
262
+ ## Complete Example
263
+
264
+ ```ruby
265
+ require 'pec_ruby'
266
+
267
+ begin
268
+ # Connect
269
+ client = PecRuby::Client.new(
270
+ host: 'imaps.pec.aruba.it',
271
+ username: 'example@pec.it',
272
+ password: 'password'
273
+ )
274
+ client.connect
275
+
276
+ # Get last 5 PEC messages
277
+ pec_messages = client.pec_messages(limit: 5)
278
+
279
+ pec_messages.each do |message|
280
+ puts "Subject: #{message.original_subject}"
281
+ puts "From: #{message.original_from}"
282
+ puts "Attachments: #{message.original_attachments.size}"
283
+
284
+ # Download attachments
285
+ message.original_attachments.each do |attachment|
286
+ attachment.save_to_dir('./downloads')
287
+ puts "Downloaded: #{attachment.filename}"
288
+ end
289
+
290
+ puts "─" * 40
291
+ end
292
+
293
+ ensure
294
+ client&.disconnect
295
+ end
296
+ ```
297
+
298
+ ## Error Handling
299
+
300
+ The gem defines several specific error classes:
301
+
302
+ ```ruby
303
+ PecRuby::Error # Base error class
304
+ PecRuby::ConnectionError # Connection issues
305
+ PecRuby::AuthenticationError # Login failures
306
+ PecRuby::MessageNotFoundError # Message not found
307
+ PecRuby::PostacertNotFoundError # postacert.eml not found
308
+ ```
309
+
310
+ Example with error handling:
311
+
312
+ ```ruby
313
+ begin
314
+ client = PecRuby::Client.new(...)
315
+ client.connect
316
+ rescue PecRuby::AuthenticationError => e
317
+ puts "Login failed: #{e.message}"
318
+ rescue PecRuby::ConnectionError => e
319
+ puts "Connection error: #{e.message}"
320
+ rescue PecRuby::Error => e
321
+ puts "PEC error: #{e.message}"
322
+ end
323
+ ```
324
+
325
+ ## Supported PEC Providers
326
+
327
+ The gem has been tested with:
328
+ - Aruba PEC (`imaps.pec.aruba.it`) ✅ **Fully tested**
329
+
330
+ Other providers should work if they support standard IMAP, but have not been tested yet.
331
+
332
+ ## Current Limitations
333
+
334
+ - **Message Threading**: The gem currently does not support message threading or conversation grouping. Each message is handled individually.
335
+ - **Provider Testing**: Only tested with Aruba PEC. Other providers may work but are not guaranteed.
336
+ - **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
+
338
+ ## Development
339
+
340
+ After cloning the repository:
341
+
342
+ ```bash
343
+ bundle install
344
+ bundle exec rspec # Run tests
345
+ bundle exec rubocop # Check code style
346
+ ```
347
+
348
+ ## Contributing
349
+
350
+ 1. Fork the project
351
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
352
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
353
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
354
+ 5. Open a Pull Request
355
+
356
+ ## License
357
+
358
+ 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/bin/pec_ruby ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/pec_ruby'
5
+
6
+ begin
7
+ if defined?(PecRuby::CLI)
8
+ PecRuby::CLI.new.run
9
+ else
10
+ puts "CLI non disponibile. Installa le dipendenze CLI:"
11
+ puts "gem install tty-prompt awesome_print"
12
+ exit 1
13
+ end
14
+ rescue LoadError => e
15
+ puts "Errore nel caricamento della CLI: #{e.message}"
16
+ puts "Installa le dipendenze CLI:"
17
+ puts "gem install tty-prompt awesome_print"
18
+ exit 1
19
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PecRuby
4
+ class Attachment
5
+ attr_reader :mail_attachment
6
+
7
+ def initialize(mail_attachment)
8
+ @mail_attachment = mail_attachment
9
+ end
10
+
11
+ def filename
12
+ @mail_attachment.filename || "unnamed_file"
13
+ end
14
+
15
+ def mime_type
16
+ @mail_attachment.mime_type || "application/octet-stream"
17
+ end
18
+
19
+ def size
20
+ content.bytesize
21
+ end
22
+
23
+ def size_kb
24
+ (size / 1024.0).round(1)
25
+ end
26
+
27
+ def size_mb
28
+ (size / 1024.0 / 1024.0).round(2)
29
+ end
30
+
31
+ def content
32
+ @mail_attachment.decoded
33
+ end
34
+
35
+ # Save attachment to file
36
+ def save_to(path)
37
+ File.binwrite(path, content)
38
+ end
39
+
40
+ # Save attachment to directory with original filename
41
+ def save_to_dir(directory)
42
+ path = File.join(directory, filename)
43
+ save_to(path)
44
+ path
45
+ end
46
+
47
+ def summary
48
+ {
49
+ filename: filename,
50
+ mime_type: mime_type,
51
+ size: size,
52
+ size_kb: size_kb,
53
+ size_mb: size_mb
54
+ }
55
+ end
56
+
57
+ def to_s
58
+ "#{filename} (#{mime_type}, #{size_kb} KB)"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'tty-prompt'
5
+ require 'awesome_print'
6
+ rescue LoadError => e
7
+ raise LoadError, "CLI dependencies not available. Install with: gem install tty-prompt awesome_print"
8
+ end
9
+
10
+ module PecRuby
11
+ class CLI
12
+ def initialize
13
+ @client = nil
14
+ @prompt = TTY::Prompt.new
15
+ end
16
+
17
+ def run
18
+ puts banner
19
+
20
+ loop do
21
+ case main_menu
22
+ when :connect
23
+ connect_to_server
24
+ when :list_messages
25
+ list_and_select_messages if connected?
26
+ when :disconnect
27
+ disconnect_from_server
28
+ when :exit
29
+ disconnect_from_server if connected?
30
+ puts "\nArrivederci!"
31
+ break
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def banner
39
+ <<~BANNER
40
+
41
+ ╔═══════════════════════════════════════════════════════════════╗
42
+ ║ PEC Decoder CLI ║
43
+ ║ Decodificatore PEC per Ruby ║
44
+ ╚═══════════════════════════════════════════════════════════════╝
45
+
46
+ BANNER
47
+ end
48
+
49
+ def main_menu
50
+ choices = []
51
+
52
+ if connected?
53
+ choices << { name: "Lista e analizza messaggi PEC", value: :list_messages }
54
+ choices << { name: "Disconnetti dal server", value: :disconnect }
55
+ else
56
+ choices << { name: "Connetti al server PEC", value: :connect }
57
+ end
58
+
59
+ choices << { name: "Esci", value: :exit }
60
+
61
+ @prompt.select("Seleziona un'azione:", choices)
62
+ end
63
+
64
+ def connect_to_server
65
+ return if connected?
66
+
67
+ puts "\nCONNESSIONE AL SERVER PEC"
68
+ puts "─" * 40
69
+
70
+ host = @prompt.ask("Host IMAP:", default: "imaps.pec.aruba.it")
71
+ username = @prompt.ask("Username/Email:")
72
+ password = @prompt.mask("Password:")
73
+
74
+ print "Connessione in corso..."
75
+
76
+ begin
77
+ @client = Client.new(host: host, username: username, password: password)
78
+ @client.connect
79
+ puts " Connesso!"
80
+ puts "Connesso come: #{@client.username}"
81
+ rescue PecRuby::Error => e
82
+ puts " Errore!"
83
+ puts "ATTENZIONE: #{e.message}"
84
+ @client = nil
85
+ end
86
+ end
87
+
88
+ def connected?
89
+ @client&.connected?
90
+ end
91
+
92
+ def disconnect_from_server
93
+ return unless connected?
94
+
95
+ @client.disconnect
96
+ @client = nil
97
+ puts "Disconnesso dal server"
98
+ end
99
+
100
+ def list_and_select_messages
101
+ puts "\nCaricamento messaggi..."
102
+
103
+ begin
104
+ messages = @client.pec_messages(limit: 20, reverse: true)
105
+
106
+ if messages.empty?
107
+ puts "Nessun messaggio PEC trovato"
108
+ return
109
+ end
110
+
111
+ choices = messages.map do |msg|
112
+ label = format_message_label(msg)
113
+ [label, msg]
114
+ end
115
+
116
+ puts "\n" + "─" * 120
117
+ puts "MESSAGGI PEC RICEVUTI (più recenti in alto)"
118
+ puts "─" * 120
119
+
120
+ selected_message = @prompt.select("Seleziona un messaggio:", choices.to_h, per_page: 15, cycle: true)
121
+ display_message(selected_message)
122
+
123
+ rescue PecRuby::Error => e
124
+ puts "Errore nel recupero messaggi: #{e.message}"
125
+ end
126
+ end
127
+
128
+ def format_message_label(message)
129
+ subject = message.subject || "(nessun oggetto)"
130
+ from = message.from || "(mittente sconosciuto)"
131
+ date = message.date ? message.date.strftime("%d/%m %H:%M") : "N/A"
132
+
133
+ short_subject = subject.length > 60 ? "#{subject[0..56]}..." : subject
134
+
135
+ sprintf("%-60s | %-25s | %s",
136
+ short_subject,
137
+ from.to_s[0..24],
138
+ date)
139
+ end
140
+
141
+ def display_message(message)
142
+ puts "\n" + "="*80
143
+ puts "MESSAGGIO PEC DECODIFICATO"
144
+ puts "="*80
145
+
146
+ # Informazioni base PEC
147
+ puts "\nINFORMAZIONI CONTENITORE PEC"
148
+ puts "─"*50
149
+ puts sprintf("Oggetto PEC: %s", message.subject || "(nessun oggetto)")
150
+ puts sprintf("From PEC: %s", message.from || "(sconosciuto)")
151
+ puts sprintf("Data PEC: %s", message.date ? message.date.strftime("%d/%m/%Y %H:%M") : "(sconosciuta)")
152
+
153
+ # Informazioni messaggio originale
154
+ if message.has_postacert?
155
+ puts "\nMESSAGGIO ORIGINALE (da postacert.eml)"
156
+ puts "─"*50
157
+ puts sprintf("Oggetto: %s", message.original_subject || "(nessun oggetto)")
158
+ puts sprintf("Mittente: %s", message.original_from || "(sconosciuto)")
159
+ puts sprintf("Destinatari: %s", message.original_to.join(', '))
160
+ puts sprintf("Data: %s", message.original_date ? message.original_date.strftime("%d/%m/%Y %H:%M") : "(sconosciuta)")
161
+
162
+ # Corpo del messaggio
163
+ body = message.original_body
164
+ if body && !body.strip.empty?
165
+ puts "\nCORPO DEL MESSAGGIO"
166
+ puts "─"*50
167
+ puts body.strip
168
+ end
169
+
170
+ # Allegati
171
+ attachments = message.original_attachments
172
+ puts "\nALLEGATI"
173
+ puts "─"*50
174
+ if attachments.any?
175
+ attachments.each_with_index do |att, i|
176
+ puts sprintf("%d. %-30s | %s | %.1f KB",
177
+ i+1,
178
+ att.filename,
179
+ att.mime_type,
180
+ att.size_kb)
181
+ end
182
+
183
+ if @prompt.yes?("\nVuoi scaricare gli allegati?")
184
+ download_attachments(attachments)
185
+ end
186
+ else
187
+ puts " Nessun allegato presente"
188
+ end
189
+ else
190
+ puts "\nATTENZIONE: Questo messaggio non contiene postacert.eml"
191
+ end
192
+
193
+ puts "\n" + "="*80
194
+ @prompt.keypress("\nPremi un tasto per continuare...")
195
+ end
196
+
197
+ def download_attachments(attachments)
198
+ download_dir = @prompt.ask("Directory di download:", default: "./downloads")
199
+
200
+ begin
201
+ require 'fileutils'
202
+ FileUtils.mkdir_p(download_dir)
203
+
204
+ attachments.each do |attachment|
205
+ file_path = attachment.save_to_dir(download_dir)
206
+ puts "Salvato: #{file_path}"
207
+ end
208
+
209
+ puts "Tutti gli allegati sono stati salvati in #{download_dir}"
210
+ rescue => e
211
+ puts "Errore nel salvataggio: #{e.message}"
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/imap'
4
+ require 'mail'
5
+
6
+ module PecRuby
7
+ class Client
8
+ attr_reader :imap, :host, :username
9
+
10
+ def initialize(host:, username:, password:, ssl: true)
11
+ @host = host
12
+ @username = username
13
+ @password = password
14
+ @ssl = ssl
15
+ @imap = nil
16
+ end
17
+
18
+ def connect
19
+ @imap = Net::IMAP.new(@host, ssl: @ssl)
20
+ authenticate
21
+ select_inbox
22
+ self
23
+ rescue Net::IMAP::Error => e
24
+ raise ConnectionError, "Failed to connect to #{@host}: #{e.message}"
25
+ end
26
+
27
+ def disconnect
28
+ return unless @imap
29
+
30
+ begin
31
+ @imap.logout if @imap && !@imap.disconnected?
32
+ rescue => e
33
+ # Ignore logout errors if connection is already closed
34
+ end
35
+
36
+ begin
37
+ @imap.disconnect if @imap && !@imap.disconnected?
38
+ rescue => e
39
+ # Ignore disconnect errors if connection is already closed
40
+ end
41
+
42
+ @imap = nil
43
+ end
44
+
45
+ def connected?
46
+ @imap && !@imap.disconnected?
47
+ end
48
+
49
+ # Get all messages or a subset
50
+ def messages(limit: nil, reverse: true)
51
+ raise ConnectionError, "Not connected" unless connected?
52
+
53
+ sequence_numbers = @imap.search(['ALL'])
54
+ sequence_numbers = sequence_numbers.reverse if reverse
55
+ sequence_numbers = sequence_numbers.first(limit) if limit
56
+
57
+ fetch_messages(sequence_numbers)
58
+ end
59
+
60
+ # Get a specific message by UID
61
+ def message(uid)
62
+ raise ConnectionError, "Not connected" unless connected?
63
+
64
+ fetch_data = @imap.uid_fetch(uid, ["UID", "ENVELOPE", "BODYSTRUCTURE"])
65
+ return nil if fetch_data.nil? || fetch_data.empty?
66
+
67
+ Message.new(self, fetch_data.first)
68
+ end
69
+
70
+ # Get messages with postacert.eml only
71
+ def pec_messages(limit: nil, reverse: true)
72
+ messages(limit: limit, reverse: reverse).select(&:has_postacert?)
73
+ end
74
+
75
+ # Internal method to fetch message body parts
76
+ def fetch_body_part(uid, part_id)
77
+ @imap.uid_fetch(uid, "BODY[#{part_id}]")[0].attr["BODY[#{part_id}]"]
78
+ end
79
+
80
+ private
81
+
82
+ def authenticate
83
+ @imap.authenticate("PLAIN", @username, @password)
84
+ rescue Net::IMAP::Error => e
85
+ raise AuthenticationError, "Authentication failed: #{e.message}"
86
+ end
87
+
88
+ def select_inbox
89
+ @imap.select('INBOX')
90
+ end
91
+
92
+ def fetch_messages(sequence_numbers)
93
+ return [] if sequence_numbers.empty?
94
+
95
+ messages_data = @imap.fetch(sequence_numbers, ["UID", "ENVELOPE", "BODYSTRUCTURE"])
96
+ messages_data.map { |msg_data| Message.new(self, msg_data) }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mail'
4
+
5
+ module PecRuby
6
+ class Message
7
+ attr_reader :uid, :envelope, :bodystructure, :client
8
+
9
+ def initialize(client, fetch_data)
10
+ @client = client
11
+ @uid = fetch_data.attr["UID"]
12
+ @envelope = fetch_data.attr["ENVELOPE"]
13
+ @bodystructure = fetch_data.attr["BODYSTRUCTURE"]
14
+ @postacert_mail = nil
15
+ @postacert_extracted = false
16
+ end
17
+
18
+ # Basic envelope information
19
+ def subject
20
+ return nil unless @envelope.subject
21
+
22
+ decoded = Mail::Encodings.value_decode(@envelope.subject)
23
+ decoded.gsub!("POSTA CERTIFICATA:", "") if decoded.start_with?("POSTA CERTIFICATA:")
24
+ decoded.strip
25
+ end
26
+
27
+ def from
28
+ return nil unless @envelope.from&.first
29
+
30
+ from_addr = @envelope.from.first
31
+ extract_real_sender(from_addr)
32
+ end
33
+
34
+ def to
35
+ return [] unless @envelope.to
36
+
37
+ @envelope.to.map { |addr| "#{addr.mailbox}@#{addr.host}" }
38
+ end
39
+
40
+ def date
41
+ @envelope.date ? Time.parse(@envelope.date.to_s) : nil
42
+ end
43
+
44
+ # Check if message contains postacert.eml
45
+ def has_postacert?
46
+ !find_postacert_part_ids.empty?
47
+ end
48
+
49
+ # Extract and return the original message from postacert.eml
50
+ def postacert_message
51
+ return @postacert_mail if @postacert_extracted
52
+
53
+ @postacert_extracted = true
54
+ part_ids = find_postacert_part_ids
55
+
56
+ if part_ids.empty?
57
+ @postacert_mail = nil
58
+ return nil
59
+ end
60
+
61
+ begin
62
+ part_id = part_ids.first
63
+ raw_data = @client.fetch_body_part(@uid, part_id)
64
+ @postacert_mail = Mail.read_from_string(raw_data)
65
+ rescue => e
66
+ raise Error, "Failed to extract postacert.eml: #{e.message}"
67
+ end
68
+
69
+ @postacert_mail
70
+ end
71
+
72
+ # Get original message subject
73
+ def original_subject
74
+ postacert_message&.subject
75
+ end
76
+
77
+ # Get original message sender
78
+ def original_from
79
+ postacert_message&.from&.first
80
+ end
81
+
82
+ # Get original message recipients
83
+ def original_to
84
+ postacert_message&.to || []
85
+ end
86
+
87
+ # Get original message date
88
+ def original_date
89
+ postacert_message&.date
90
+ end
91
+
92
+ # Get original message body (text/plain preferred)
93
+ def original_body
94
+ mail = postacert_message
95
+ return nil unless mail
96
+
97
+ text_part = extract_text_part(mail, "text/plain")
98
+ html_part = extract_text_part(mail, "text/html")
99
+ selected_part = text_part || html_part
100
+
101
+ return nil unless selected_part
102
+
103
+ raw_body = selected_part.body.decoded
104
+ charset = selected_part.charset ||
105
+ selected_part.content_type_parameters&.[]("charset") ||
106
+ "UTF-8"
107
+
108
+ raw_body.force_encoding(charset).encode("UTF-8")
109
+ end
110
+
111
+ # Get original message attachments
112
+ def original_attachments
113
+ mail = postacert_message
114
+ return [] unless mail&.attachments
115
+
116
+ mail.attachments.map { |att| Attachment.new(att) }
117
+ end
118
+
119
+ # Summary information
120
+ def summary
121
+ {
122
+ uid: @uid,
123
+ subject: subject,
124
+ from: from,
125
+ to: to,
126
+ date: date,
127
+ has_postacert: has_postacert?,
128
+ original_subject: original_subject,
129
+ original_from: original_from,
130
+ original_to: original_to,
131
+ original_date: original_date,
132
+ attachments_count: original_attachments.size
133
+ }
134
+ end
135
+
136
+ private
137
+
138
+ def extract_real_sender(from_addr)
139
+ email = "#{from_addr.mailbox}@#{from_addr.host}"
140
+
141
+ # Handle "Per conto di:" in name field
142
+ if from_addr.name&.include?("Per conto di:")
143
+ email_match = from_addr.name.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/)
144
+ return email_match[1] if email_match
145
+ elsif from_addr.name && !from_addr.name.include?("posta-certificata@")
146
+ return from_addr.name
147
+ end
148
+
149
+ email
150
+ end
151
+
152
+ def find_postacert_part_ids(bodystructure = @bodystructure, path = "")
153
+ results = []
154
+
155
+ if bodystructure.respond_to?(:parts) && bodystructure.parts
156
+ bodystructure.parts.each_with_index do |part, index|
157
+ part_path = path.empty? ? "#{index + 1}" : "#{path}.#{index + 1}"
158
+ results += find_postacert_part_ids(part, part_path)
159
+ end
160
+ elsif bodystructure.media_type == "MESSAGE" && bodystructure.subtype == "RFC822"
161
+ if bodystructure.param && bodystructure.param["NAME"]&.downcase&.include?("postacert.eml")
162
+ results << path
163
+ end
164
+ end
165
+
166
+ results
167
+ end
168
+
169
+ 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
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PecRuby
4
+ VERSION = "0.1.0"
5
+ end
data/lib/pec_ruby.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pec_ruby/version"
4
+ require_relative "pec_ruby/client"
5
+ require_relative "pec_ruby/message"
6
+ require_relative "pec_ruby/attachment"
7
+
8
+ # CLI is optional - only load if dependencies are available
9
+ begin
10
+ require_relative "pec_ruby/cli"
11
+ rescue LoadError
12
+ # CLI dependencies not available - skip CLI functionality
13
+ end
14
+
15
+ module PecRuby
16
+ class Error < StandardError; end
17
+ class ConnectionError < Error; end
18
+ class AuthenticationError < Error; end
19
+ class MessageNotFoundError < Error; end
20
+ class PostacertNotFoundError < Error; end
21
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pec_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - EMG
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-07-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mail
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-imap
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
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.
72
+ email:
73
+ - enricomaria.giordano@icloud.com
74
+ executables:
75
+ - pec_ruby
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - CHANGELOG.md
80
+ - Gemfile
81
+ - LICENSE
82
+ - README.md
83
+ - bin/pec_ruby
84
+ - lib/pec_ruby.rb
85
+ - lib/pec_ruby/attachment.rb
86
+ - lib/pec_ruby/cli.rb
87
+ - lib/pec_ruby/client.rb
88
+ - lib/pec_ruby/message.rb
89
+ - lib/pec_ruby/version.rb
90
+ homepage: https://github.com/egio12/pec_ruby
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ allowed_push_host: https://rubygems.org
95
+ homepage_uri: https://github.com/egio12/pec_ruby
96
+ source_code_uri: https://github.com/egio12/pec_ruby
97
+ changelog_uri: https://github.com/egio12/pec_ruby/blob/main/CHANGELOG.md
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 2.6.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.4.19
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Ruby gem for decoding and reading Italian PEC (Posta Elettronica Certificata)
117
+ emails
118
+ test_files: []