pec_ruby 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec_status +115 -98
- data/CHANGELOG.md +52 -1
- data/Gemfile.lock +3 -1
- data/README.md +212 -45
- data/lib/pec_ruby/cli.rb +353 -100
- data/lib/pec_ruby/client.rb +77 -29
- data/lib/pec_ruby/message.rb +188 -38
- data/lib/pec_ruby/version.rb +2 -2
- data/lib/pec_ruby.rb +7 -6
- metadata +16 -2
data/lib/pec_ruby/cli.rb
CHANGED
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
begin
|
4
4
|
require 'tty-prompt'
|
5
|
+
require 'tty-screen'
|
5
6
|
require 'awesome_print'
|
6
7
|
rescue LoadError => e
|
7
|
-
raise LoadError,
|
8
|
+
raise LoadError, 'CLI dependencies not available. Install with: gem install tty-prompt tty-screen awesome_print'
|
8
9
|
end
|
9
10
|
|
10
11
|
module PecRuby
|
@@ -12,22 +13,29 @@ module PecRuby
|
|
12
13
|
def initialize
|
13
14
|
@client = nil
|
14
15
|
@prompt = TTY::Prompt.new
|
16
|
+
@screen_height = TTY::Screen.height
|
17
|
+
@screen_width = TTY::Screen.width
|
15
18
|
end
|
16
19
|
|
17
20
|
def run
|
18
|
-
puts banner
|
19
|
-
|
20
21
|
loop do
|
22
|
+
clear_screen
|
23
|
+
show_header
|
24
|
+
show_status
|
25
|
+
|
21
26
|
case main_menu
|
22
27
|
when :connect
|
23
28
|
connect_to_server
|
29
|
+
when :select_folder
|
30
|
+
select_folder_menu if connected?
|
24
31
|
when :list_messages
|
25
32
|
list_and_select_messages if connected?
|
26
33
|
when :disconnect
|
27
34
|
disconnect_from_server
|
28
35
|
when :exit
|
29
36
|
disconnect_from_server if connected?
|
30
|
-
|
37
|
+
clear_screen
|
38
|
+
puts center_text('Arrivederci!')
|
31
39
|
break
|
32
40
|
end
|
33
41
|
end
|
@@ -35,28 +43,56 @@ module PecRuby
|
|
35
43
|
|
36
44
|
private
|
37
45
|
|
46
|
+
def clear_screen
|
47
|
+
print "\033[2J\033[H"
|
48
|
+
end
|
49
|
+
|
50
|
+
def center_text(text)
|
51
|
+
padding = (@screen_width - text.length) / 2
|
52
|
+
' ' * [padding, 0].max + text
|
53
|
+
end
|
54
|
+
|
55
|
+
def show_header
|
56
|
+
puts banner
|
57
|
+
end
|
58
|
+
|
59
|
+
def show_status
|
60
|
+
if connected?
|
61
|
+
status = "CONNESSO: #{@client.username}"
|
62
|
+
folder = @client.current_folder ? " | FOLDER: #{@client.current_folder}" : ""
|
63
|
+
puts center_text("#{status}#{folder}")
|
64
|
+
else
|
65
|
+
puts center_text("NON CONNESSO")
|
66
|
+
end
|
67
|
+
puts "─" * @screen_width
|
68
|
+
puts
|
69
|
+
end
|
70
|
+
|
38
71
|
def banner
|
39
72
|
<<~BANNER
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
73
|
+
██████╗ ███████╗ ██████╗ ██████╗ ██╗ ██╗██████╗ ██╗ ██╗
|
74
|
+
██╔══██╗██╔════╝██╔════╝ ██╔══██╗██║ ██║██╔══██╗╚██╗ ██╔╝
|
75
|
+
██████╔╝█████╗ ██║ ██████╔╝██║ ██║██████╔╝ ╚████╔╝
|
76
|
+
██╔═══╝ ██╔══╝ ██║ ██╔══██╗██║ ██║██╔══██╗ ╚██╔╝
|
77
|
+
██║ ███████╗╚██████╗ ██║ ██║╚██████╔╝██████╔╝ ██║
|
78
|
+
╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝
|
79
|
+
|
80
|
+
PEC Ruby CLI v#{PecRuby::VERSION} | Italian PEC Email Manager
|
46
81
|
BANNER
|
47
82
|
end
|
48
83
|
|
49
84
|
def main_menu
|
50
85
|
choices = []
|
51
|
-
|
86
|
+
|
52
87
|
if connected?
|
53
|
-
choices << { name:
|
54
|
-
choices << { name:
|
88
|
+
choices << { name: 'Seleziona folder', value: :select_folder }
|
89
|
+
choices << { name: 'Lista e analizza messaggi', value: :list_messages }
|
90
|
+
choices << { name: 'Disconnetti dal server', value: :disconnect }
|
55
91
|
else
|
56
|
-
choices << { name:
|
92
|
+
choices << { name: 'Connetti al server PEC', value: :connect }
|
57
93
|
end
|
58
|
-
|
59
|
-
choices << { name:
|
94
|
+
|
95
|
+
choices << { name: 'Esci', value: :exit }
|
60
96
|
|
61
97
|
@prompt.select("Seleziona un'azione:", choices)
|
62
98
|
end
|
@@ -64,24 +100,35 @@ module PecRuby
|
|
64
100
|
def connect_to_server
|
65
101
|
return if connected?
|
66
102
|
|
67
|
-
|
68
|
-
|
103
|
+
clear_screen
|
104
|
+
show_header
|
105
|
+
puts
|
106
|
+
puts center_text("CONNESSIONE AL SERVER PEC")
|
107
|
+
puts "─" * @screen_width
|
108
|
+
puts
|
69
109
|
|
70
|
-
host = @prompt.ask(
|
71
|
-
username = @prompt.ask(
|
72
|
-
password = @prompt.mask(
|
110
|
+
host = @prompt.ask('Host IMAP:', default: 'imaps.pec.aruba.it')
|
111
|
+
username = @prompt.ask('Username/Email:')
|
112
|
+
password = @prompt.mask('Password:')
|
113
|
+
|
114
|
+
puts
|
115
|
+
puts center_text('[*] Connessione in corso...')
|
73
116
|
|
74
|
-
print "Connessione in corso..."
|
75
|
-
|
76
117
|
begin
|
77
118
|
@client = Client.new(host: host, username: username, password: password)
|
78
119
|
@client.connect
|
79
|
-
|
80
|
-
puts
|
120
|
+
|
121
|
+
puts center_text('[+] Connesso con successo!')
|
122
|
+
puts center_text("Utente: #{@client.username}")
|
123
|
+
puts center_text("Folder: INBOX")
|
124
|
+
|
125
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
81
126
|
rescue PecRuby::Error => e
|
82
|
-
puts
|
83
|
-
puts "
|
127
|
+
puts center_text('[-] Errore di connessione!')
|
128
|
+
puts center_text("Dettagli: #{e.message}")
|
84
129
|
@client = nil
|
130
|
+
|
131
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
85
132
|
end
|
86
133
|
end
|
87
134
|
|
@@ -91,20 +138,39 @@ module PecRuby
|
|
91
138
|
|
92
139
|
def disconnect_from_server
|
93
140
|
return unless connected?
|
141
|
+
|
142
|
+
clear_screen
|
143
|
+
show_header
|
144
|
+
puts
|
145
|
+
puts center_text('[*] Disconnessione in corso...')
|
94
146
|
|
95
147
|
@client.disconnect
|
96
148
|
@client = nil
|
97
|
-
|
149
|
+
|
150
|
+
puts center_text('[+] Disconnesso dal server')
|
151
|
+
|
152
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
98
153
|
end
|
99
154
|
|
100
155
|
def list_and_select_messages
|
101
|
-
|
156
|
+
current_folder = @client.current_folder || 'INBOX'
|
102
157
|
|
158
|
+
clear_screen
|
159
|
+
show_header
|
160
|
+
show_status
|
161
|
+
puts center_text("MESSAGGI DA #{current_folder.upcase}")
|
162
|
+
puts "─" * @screen_width
|
163
|
+
puts center_text("[*] Caricamento messaggi...")
|
164
|
+
|
103
165
|
begin
|
104
|
-
messages
|
105
|
-
|
166
|
+
# Get messages in reverse order (newest first) directly from server
|
167
|
+
start_time = Time.now
|
168
|
+
messages = @client.messages(limit: 50, reverse: true)
|
169
|
+
load_time = Time.now - start_time
|
170
|
+
|
106
171
|
if messages.empty?
|
107
|
-
puts "Nessun messaggio
|
172
|
+
puts center_text("[-] Nessun messaggio trovato in #{current_folder}")
|
173
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
108
174
|
return
|
109
175
|
end
|
110
176
|
|
@@ -113,103 +179,290 @@ module PecRuby
|
|
113
179
|
[label, msg]
|
114
180
|
end
|
115
181
|
|
116
|
-
puts "
|
117
|
-
puts
|
118
|
-
|
119
|
-
|
120
|
-
selected_message = @prompt.select("Seleziona un messaggio:", choices.to_h, per_page: 15, cycle: true)
|
182
|
+
puts center_text("[+] Trovati #{messages.size} messaggi (caricati in #{load_time.round(2)}s)")
|
183
|
+
puts
|
184
|
+
|
185
|
+
selected_message = @prompt.select('Seleziona un messaggio:', choices.to_h, per_page: 15, cycle: true)
|
121
186
|
display_message(selected_message)
|
122
|
-
|
123
187
|
rescue PecRuby::Error => e
|
124
|
-
puts "Errore nel recupero messaggi: #{e.message}"
|
188
|
+
puts center_text("[-] Errore nel recupero messaggi: #{e.message}")
|
189
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
190
|
+
rescue StandardError => e
|
191
|
+
puts center_text("[-] Errore imprevisto: #{e.message}")
|
192
|
+
puts center_text("Dettagli: #{e.class}")
|
193
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
125
194
|
end
|
126
195
|
end
|
127
196
|
|
128
197
|
def format_message_label(message)
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
198
|
+
# Use only original (envelope) data for performance - no postacert fetching
|
199
|
+
subject = message.original_subject || '(nessun oggetto)'
|
200
|
+
from = message.original_from || '(mittente sconosciuto)'
|
201
|
+
date = message.original_date ? message.original_date.strftime('%d/%m %H:%M') : 'N/A'
|
202
|
+
|
133
203
|
short_subject = subject.length > 60 ? "#{subject[0..56]}..." : subject
|
134
|
-
|
135
|
-
|
136
|
-
short_subject,
|
137
|
-
from.to_s[0..24],
|
204
|
+
|
205
|
+
format('%-60s | %-25s | %s',
|
206
|
+
short_subject,
|
207
|
+
from.to_s[0..24],
|
138
208
|
date)
|
139
209
|
end
|
140
210
|
|
141
211
|
def display_message(message)
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
puts
|
158
|
-
puts
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
212
|
+
loop do
|
213
|
+
clear_screen
|
214
|
+
show_header
|
215
|
+
show_status
|
216
|
+
|
217
|
+
# Message header with visual separator
|
218
|
+
puts center_text("DETTAGLIO MESSAGGIO")
|
219
|
+
puts "═" * @screen_width
|
220
|
+
puts
|
221
|
+
|
222
|
+
# Message info in organized sections
|
223
|
+
display_message_info(message)
|
224
|
+
display_message_body(message, truncate: true)
|
225
|
+
display_message_attachments(message, show_download_option: false)
|
226
|
+
|
227
|
+
puts
|
228
|
+
puts "═" * @screen_width
|
229
|
+
|
230
|
+
# Show message menu
|
231
|
+
action = show_message_menu(message)
|
232
|
+
case action
|
233
|
+
when :full_body
|
234
|
+
display_full_body(message)
|
235
|
+
when :download_attachments
|
236
|
+
download_attachments(message.attachments) if message.attachments.any?
|
237
|
+
when :back
|
238
|
+
break
|
168
239
|
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def show_message_menu(message)
|
244
|
+
choices = []
|
245
|
+
|
246
|
+
# Add body option if message has body
|
247
|
+
body = message.raw_body
|
248
|
+
if body && body[:content] && !body[:content].strip.empty?
|
249
|
+
choices << { name: "Visualizza corpo completo", value: :full_body }
|
250
|
+
end
|
251
|
+
|
252
|
+
# Add attachments option if message has attachments
|
253
|
+
if message.attachments.any?
|
254
|
+
choices << { name: "Scarica allegati (#{message.attachments.size})", value: :download_attachments }
|
255
|
+
end
|
256
|
+
|
257
|
+
choices << { name: "← Torna alla lista", value: :back }
|
258
|
+
|
259
|
+
@prompt.select("Seleziona un'azione:", choices)
|
260
|
+
end
|
169
261
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
262
|
+
def display_full_body(message)
|
263
|
+
clear_screen
|
264
|
+
show_header
|
265
|
+
|
266
|
+
puts center_text("CORPO COMPLETO DEL MESSAGGIO")
|
267
|
+
puts "═" * @screen_width
|
268
|
+
puts
|
269
|
+
|
270
|
+
body = message.raw_body
|
271
|
+
if body && body[:content] && !body[:content].strip.empty?
|
272
|
+
content = body[:content].strip
|
273
|
+
content = content.gsub(/\r\n/, "\n")
|
274
|
+
content = content.gsub(/\u0093|\u0094/, '"')
|
275
|
+
content = content.gsub(/\u0092/, "'")
|
276
|
+
|
277
|
+
puts content
|
278
|
+
else
|
279
|
+
puts center_text("Nessun contenuto disponibile")
|
280
|
+
end
|
281
|
+
|
282
|
+
puts
|
283
|
+
puts "═" * @screen_width
|
284
|
+
@prompt.keypress(center_text("Premi un tasto per continuare..."), echo: false)
|
285
|
+
end
|
286
|
+
|
287
|
+
def display_message_info(message)
|
288
|
+
# Main information section
|
289
|
+
puts "[INFORMAZIONI PRINCIPALI]"
|
290
|
+
puts "─" * (@screen_width - 20)
|
291
|
+
|
292
|
+
info_lines = [
|
293
|
+
["Oggetto", message.subject || "(nessun oggetto)"],
|
294
|
+
["Mittente", message.from || "(sconosciuto)"],
|
295
|
+
["Destinatari", message.to.join(", ")],
|
296
|
+
["Data", message.date ? message.date.strftime("%d/%m/%Y %H:%M") : "(sconosciuta)"]
|
297
|
+
]
|
298
|
+
|
299
|
+
display_info_table(info_lines)
|
300
|
+
puts
|
301
|
+
|
302
|
+
# PEC container information
|
303
|
+
puts "[INFORMAZIONI CONTENITORE PEC]"
|
304
|
+
puts "─" * (@screen_width - 20)
|
305
|
+
|
306
|
+
pec_lines = [
|
307
|
+
["Oggetto PEC", message.original_subject || "(nessun oggetto)"],
|
308
|
+
["From PEC", message.original_from || "(sconosciuto)"],
|
309
|
+
["Data PEC", message.original_date ? message.original_date.strftime("%d/%m/%Y %H:%M") : "(sconosciuta)"]
|
310
|
+
]
|
311
|
+
|
312
|
+
display_info_table(pec_lines)
|
313
|
+
puts
|
314
|
+
end
|
315
|
+
|
316
|
+
def display_message_body(message, truncate: false)
|
317
|
+
body = message.raw_body
|
318
|
+
return unless body && body[:content] && !body[:content].strip.empty?
|
319
|
+
|
320
|
+
puts "[CORPO DEL MESSAGGIO]"
|
321
|
+
puts "─" * (@screen_width - 20)
|
322
|
+
|
323
|
+
# Format the body for better readability
|
324
|
+
content = body[:content].strip
|
325
|
+
content = content.gsub(/\r\n/, "\n")
|
326
|
+
content = content.gsub(/\u0093|\u0094/, '"')
|
327
|
+
content = content.gsub(/\u0092/, "'")
|
328
|
+
|
329
|
+
if truncate
|
330
|
+
# Truncate long messages for overview
|
331
|
+
max_lines = 10
|
332
|
+
lines = content.split("\n")
|
333
|
+
|
334
|
+
if lines.length > max_lines
|
335
|
+
puts lines.first(max_lines).join("\n")
|
336
|
+
puts
|
337
|
+
puts center_text("... (messaggio troncato, #{lines.length - max_lines} righe rimanenti)")
|
186
338
|
else
|
187
|
-
puts
|
339
|
+
puts content
|
188
340
|
end
|
189
341
|
else
|
190
|
-
puts
|
342
|
+
puts content
|
191
343
|
end
|
344
|
+
puts
|
345
|
+
end
|
346
|
+
|
347
|
+
def display_message_attachments(message, show_download_option: true)
|
348
|
+
attachments = message.attachments
|
349
|
+
puts "[ALLEGATI - #{attachments.size}]"
|
350
|
+
puts "─" * (@screen_width - 20)
|
192
351
|
|
193
|
-
|
194
|
-
|
352
|
+
if attachments.any?
|
353
|
+
attachments.each_with_index do |att, i|
|
354
|
+
status = att.filename.include?('postacert') ? '[PEC]' : '[ATT]'
|
355
|
+
puts format("%s %2d. %-35s | %-20s | %8.1f KB",
|
356
|
+
status,
|
357
|
+
i + 1,
|
358
|
+
truncate_text(att.filename, 35),
|
359
|
+
truncate_text(att.mime_type, 20),
|
360
|
+
att.size_kb)
|
361
|
+
end
|
362
|
+
|
363
|
+
if show_download_option
|
364
|
+
puts
|
365
|
+
download_attachments(attachments) if @prompt.yes?("Vuoi scaricare gli allegati?")
|
366
|
+
end
|
367
|
+
else
|
368
|
+
puts center_text("Nessun allegato presente")
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def display_info_table(lines)
|
373
|
+
max_label_width = lines.map { |line| line[0].length }.max
|
374
|
+
|
375
|
+
lines.each do |label, value|
|
376
|
+
formatted_label = label.ljust(max_label_width)
|
377
|
+
puts " #{formatted_label} : #{value}"
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def truncate_text(text, max_length)
|
382
|
+
text.length > max_length ? "#{text[0..max_length-4]}..." : text
|
383
|
+
end
|
384
|
+
|
385
|
+
def select_folder_menu
|
386
|
+
clear_screen
|
387
|
+
show_header
|
388
|
+
show_status
|
389
|
+
puts center_text("SELEZIONE FOLDER")
|
390
|
+
puts "─" * @screen_width
|
391
|
+
puts
|
392
|
+
|
393
|
+
begin
|
394
|
+
folders = @client.available_folders
|
395
|
+
|
396
|
+
if folders.empty?
|
397
|
+
puts center_text('[-] Nessuna folder disponibile')
|
398
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
399
|
+
return
|
400
|
+
end
|
401
|
+
|
402
|
+
current_folder = @client.current_folder || 'INBOX'
|
403
|
+
puts center_text("Folder corrente: #{current_folder}")
|
404
|
+
puts
|
405
|
+
|
406
|
+
folder_choices = folders.map { |folder| { name: "#{folder == current_folder ? '[*]' : ' '} #{folder}", value: folder } }
|
407
|
+
folder_choices << { name: '← Torna al menu principale', value: :back }
|
408
|
+
|
409
|
+
selected_folder = @prompt.select('Seleziona una folder:', folder_choices)
|
410
|
+
|
411
|
+
return if selected_folder == :back
|
412
|
+
|
413
|
+
if selected_folder != current_folder
|
414
|
+
puts center_text("[*] Selezione folder #{selected_folder}...")
|
415
|
+
@client.select_folder(selected_folder)
|
416
|
+
puts center_text('[+] Folder selezionata con successo!')
|
417
|
+
puts center_text("Folder attiva: #{selected_folder}")
|
418
|
+
else
|
419
|
+
puts center_text("[*] Folder #{selected_folder} già selezionata")
|
420
|
+
end
|
421
|
+
|
422
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
423
|
+
rescue PecRuby::Error => e
|
424
|
+
puts center_text("[-] Errore nella selezione folder: #{e.message}")
|
425
|
+
@prompt.keypress("\n#{center_text('Premi un tasto per continuare...')}", echo: false)
|
426
|
+
end
|
195
427
|
end
|
196
428
|
|
197
429
|
def download_attachments(attachments)
|
198
|
-
|
430
|
+
clear_screen
|
431
|
+
show_header
|
432
|
+
show_status
|
433
|
+
|
434
|
+
puts center_text("DOWNLOAD ALLEGATI")
|
435
|
+
puts "═" * @screen_width
|
436
|
+
puts
|
437
|
+
|
438
|
+
download_dir = @prompt.ask('Directory di download:', default: './downloads')
|
439
|
+
|
440
|
+
puts center_text("[*] Creazione directory...")
|
199
441
|
|
200
442
|
begin
|
201
443
|
require 'fileutils'
|
202
444
|
FileUtils.mkdir_p(download_dir)
|
203
445
|
|
204
|
-
|
446
|
+
puts center_text("[+] Directory creata: #{download_dir}")
|
447
|
+
puts
|
448
|
+
|
449
|
+
attachments.each_with_index do |attachment, i|
|
450
|
+
puts center_text("[*] Salvando #{i+1}/#{attachments.size}: #{attachment.filename}")
|
205
451
|
file_path = attachment.save_to_dir(download_dir)
|
206
|
-
puts "Salvato: #{file_path}"
|
452
|
+
puts center_text("[+] Salvato: #{file_path}")
|
207
453
|
end
|
208
454
|
|
209
|
-
puts
|
210
|
-
|
211
|
-
puts "
|
455
|
+
puts
|
456
|
+
puts center_text("[+] Tutti gli allegati sono stati salvati!")
|
457
|
+
puts center_text("Directory: #{download_dir}")
|
458
|
+
|
459
|
+
rescue StandardError => e
|
460
|
+
puts center_text("[-] Errore nel salvataggio: #{e.message}")
|
212
461
|
end
|
462
|
+
|
463
|
+
puts
|
464
|
+
puts "═" * @screen_width
|
465
|
+
@prompt.keypress(center_text("Premi un tasto per continuare..."), echo: false)
|
213
466
|
end
|
214
467
|
end
|
215
|
-
end
|
468
|
+
end
|