pec_ruby 0.2.2 → 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.
@@ -5,7 +5,7 @@ require 'mail'
5
5
 
6
6
  module PecRuby
7
7
  class Client
8
- attr_reader :imap, :host, :username
8
+ attr_reader :imap, :host, :username, :current_folder
9
9
 
10
10
  def initialize(host:, username:, password:, ssl: true)
11
11
  @host = host
@@ -13,6 +13,7 @@ module PecRuby
13
13
  @password = password
14
14
  @ssl = ssl
15
15
  @imap = nil
16
+ @current_folder = nil
16
17
  end
17
18
 
18
19
  def connect
@@ -29,48 +30,105 @@ module PecRuby
29
30
 
30
31
  begin
31
32
  @imap.logout if @imap && !@imap.disconnected?
32
- rescue => e
33
+ rescue StandardError => e
33
34
  # Ignore logout errors if connection is already closed
34
35
  end
35
-
36
+
36
37
  begin
37
38
  @imap.disconnect if @imap && !@imap.disconnected?
38
- rescue => e
39
+ rescue StandardError => e
39
40
  # Ignore disconnect errors if connection is already closed
40
41
  end
41
-
42
+
42
43
  @imap = nil
43
44
  end
44
45
 
45
46
  def connected?
46
47
  return false unless @imap
48
+
47
49
  !@imap.disconnected?
48
50
  end
49
51
 
52
+ # Get the list of available folders in the mailbox
53
+ def available_folders
54
+ raise ConnectionError, 'Not connected' unless connected?
55
+
56
+ @imap.list('', '*').map(&:name)
57
+ end
58
+
59
+ def select_folder(folder)
60
+ raise ConnectionError, 'Not connected' unless connected?
61
+
62
+ # Check if the folder exists
63
+ raise FolderError, "Folder '#{folder}' does not exist" unless available_folders.include?(folder)
64
+
65
+ @imap.select(folder)
66
+ @current_folder = folder
67
+ rescue Net::IMAP::Error => e
68
+ raise FolderError, "Failed to select folder '#{folder}': #{e.message}"
69
+ end
70
+
71
+ def select_inbox
72
+ select_folder('INBOX')
73
+ @current_folder = 'INBOX'
74
+ rescue FolderError => e
75
+ raise FolderError, "Failed to select INBOX: #{e.message}"
76
+ end
77
+
50
78
  # Get all messages or a subset
51
79
  def messages(limit: nil, reverse: true)
52
- raise ConnectionError, "Not connected" unless connected?
80
+ raise ConnectionError, 'Not connected' unless connected?
81
+
82
+ # Use search to get available sequence numbers (safer approach)
83
+ begin
84
+ sequence_numbers = @imap.search(['ALL'])
85
+ rescue Net::IMAP::Error => e
86
+ raise ConnectionError, "Failed to search messages: #{e.message}"
87
+ end
53
88
 
54
- sequence_numbers = @imap.search(['ALL'])
55
- sequence_numbers = sequence_numbers.reverse if reverse
56
- sequence_numbers = sequence_numbers.first(limit) if limit
89
+ return [] if sequence_numbers.empty?
90
+
91
+ # Sort and limit the sequence numbers
92
+ if reverse
93
+ sequence_numbers = sequence_numbers.sort.reverse
94
+ else
95
+ sequence_numbers = sequence_numbers.sort
96
+ end
57
97
 
58
- fetch_messages(sequence_numbers)
98
+ # Apply limit if specified
99
+ sequence_numbers = sequence_numbers.first(limit) if limit
100
+
101
+ fetch_messages_by_sequence(sequence_numbers)
59
102
  end
60
103
 
61
104
  # Get a specific message by UID
62
105
  def message(uid)
63
- raise ConnectionError, "Not connected" unless connected?
64
-
65
- fetch_data = @imap.uid_fetch(uid, ["UID", "ENVELOPE", "BODYSTRUCTURE"])
106
+ raise ConnectionError, 'Not connected' unless connected?
107
+
108
+ fetch_data = @imap.uid_fetch(uid, %w[UID ENVELOPE BODYSTRUCTURE])
66
109
  return nil if fetch_data.nil? || fetch_data.empty?
67
-
110
+
68
111
  Message.new(self, fetch_data.first)
69
112
  end
70
113
 
71
- # Get messages with postacert.eml only
72
- def pec_messages(limit: nil, reverse: true)
73
- messages(limit: limit, reverse: reverse).select(&:has_postacert?)
114
+
115
+ # Internal method to fetch messages by sequence number
116
+ def fetch_messages_by_sequence(sequence_numbers)
117
+ return [] if sequence_numbers.empty?
118
+
119
+ # Fetch messages in batches to avoid memory issues
120
+ batch_size = 50
121
+ all_messages = []
122
+
123
+ sequence_numbers.each_slice(batch_size) do |seq_batch|
124
+ fetch_data = @imap.fetch(seq_batch, %w[UID ENVELOPE BODYSTRUCTURE])
125
+ next if fetch_data.nil?
126
+
127
+ batch_messages = fetch_data.map { |data| Message.new(self, data) }
128
+ all_messages.concat(batch_messages)
129
+ end
130
+
131
+ all_messages
74
132
  end
75
133
 
76
134
  # Internal method to fetch message body parts
@@ -81,20 +139,10 @@ module PecRuby
81
139
  private
82
140
 
83
141
  def authenticate
84
- @imap.authenticate("PLAIN", @username, @password)
142
+ @imap.authenticate('PLAIN', @username, @password)
85
143
  rescue Net::IMAP::Error => e
86
144
  raise AuthenticationError, "Authentication failed: #{e.message}"
87
145
  end
88
146
 
89
- def select_inbox
90
- @imap.select('INBOX')
91
- end
92
-
93
- def fetch_messages(sequence_numbers)
94
- return [] if sequence_numbers.empty?
95
-
96
- messages_data = @imap.fetch(sequence_numbers, ["UID", "ENVELOPE", "BODYSTRUCTURE"])
97
- messages_data.map { |msg_data| Message.new(self, msg_data) }
98
- end
99
147
  end
100
- end
148
+ end
@@ -15,8 +15,41 @@ module PecRuby
15
15
  @postacert_extracted = false
16
16
  end
17
17
 
18
- # Basic envelope information
18
+ # Basic envelope information - now points to postacert.eml when available
19
19
  def subject
20
+ if has_postacert?
21
+ postacert_message&.subject
22
+ else
23
+ original_subject
24
+ end
25
+ end
26
+
27
+ def from
28
+ if has_postacert?
29
+ postacert_message&.from&.first
30
+ else
31
+ original_from
32
+ end
33
+ end
34
+
35
+ def to
36
+ if has_postacert?
37
+ postacert_message&.to || []
38
+ else
39
+ original_to
40
+ end
41
+ end
42
+
43
+ def date
44
+ if has_postacert?
45
+ postacert_message&.date
46
+ else
47
+ original_date
48
+ end
49
+ end
50
+
51
+ # Original message envelope information (the outer PEC container)
52
+ def original_subject
20
53
  return nil unless @envelope.subject
21
54
 
22
55
  decoded = Mail::Encodings.value_decode(@envelope.subject)
@@ -24,20 +57,20 @@ module PecRuby
24
57
  decoded.strip
25
58
  end
26
59
 
27
- def from
60
+ def original_from
28
61
  return nil unless @envelope.from&.first
29
62
 
30
63
  from_addr = @envelope.from.first
31
64
  extract_real_sender(from_addr)
32
65
  end
33
66
 
34
- def to
67
+ def original_to
35
68
  return [] unless @envelope.to
36
69
 
37
70
  @envelope.to.map { |addr| "#{addr.mailbox}@#{addr.host}" }
38
71
  end
39
72
 
40
- def date
73
+ def original_date
41
74
  @envelope.date ? Time.parse(@envelope.date.to_s) : nil
42
75
  end
43
76
 
@@ -69,28 +102,8 @@ module PecRuby
69
102
  @postacert_mail
70
103
  end
71
104
 
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 with format information
93
- def original_body
105
+ # Get postacert message body with format information
106
+ def postacert_body
94
107
  mail = postacert_message
95
108
  return nil unless mail
96
109
 
@@ -116,8 +129,8 @@ module PecRuby
116
129
  }
117
130
  end
118
131
 
119
- # Get original message body as plain text only
120
- def original_body_text
132
+ # Get postacert message body as plain text only
133
+ def postacert_body_text
121
134
  mail = postacert_message
122
135
  return nil unless mail
123
136
 
@@ -132,8 +145,8 @@ module PecRuby
132
145
  raw_body.dup.force_encoding(charset).encode("UTF-8")
133
146
  end
134
147
 
135
- # Get original message body as HTML only
136
- def original_body_html
148
+ # Get postacert message body as HTML only
149
+ def postacert_body_html
137
150
  mail = postacert_message
138
151
  return nil unless mail
139
152
 
@@ -148,13 +161,40 @@ module PecRuby
148
161
  raw_body.dup.force_encoding(charset).encode("UTF-8")
149
162
  end
150
163
 
151
- # Get original message attachments (with memoization)
152
- def original_attachments
153
- @original_attachments ||= begin
164
+ # Get message body - preferring postacert.eml if available, otherwise direct message
165
+ def raw_body
166
+ if has_postacert?
167
+ postacert_body
168
+ else
169
+ direct_message_body
170
+ end
171
+ end
172
+
173
+ # Get message body as plain text - preferring postacert.eml if available, otherwise direct message
174
+ def raw_body_text
175
+ if has_postacert?
176
+ postacert_body_text
177
+ else
178
+ direct_message_body_text
179
+ end
180
+ end
181
+
182
+ # Get message body as HTML - preferring postacert.eml if available, otherwise direct message
183
+ def raw_body_html
184
+ if has_postacert?
185
+ postacert_body_html
186
+ else
187
+ direct_message_body_html
188
+ end
189
+ end
190
+
191
+ # Get postacert message attachments (with memoization)
192
+ def postacert_attachments
193
+ @postacert_attachments ||= begin
154
194
  mail = postacert_message
155
195
  attachments = []
156
196
 
157
- # Add attachments from the original message
197
+ # Add attachments from the postacert message
158
198
  if mail&.attachments
159
199
  attachments += mail.attachments.map { |att| Attachment.new(att) }
160
200
  end
@@ -167,14 +207,53 @@ module PecRuby
167
207
  end
168
208
  end
169
209
 
170
- # Get original message attachments that are NOT postacert.eml files
210
+ # Get postacert message attachments that are NOT postacert.eml files
211
+ def postacert_regular_attachments
212
+ postacert_attachments.reject(&:postacert?)
213
+ end
214
+
215
+ # Get attachments - preferring postacert.eml if available, otherwise direct message
216
+ def attachments
217
+ if has_postacert?
218
+ postacert_attachments
219
+ else
220
+ [] # Direct messages typically don't have attachments via IMAP
221
+ end
222
+ end
223
+
224
+ # Get regular attachments (non-postacert.eml)
225
+ def regular_attachments
226
+ if has_postacert?
227
+ postacert_regular_attachments
228
+ else
229
+ [] # Direct messages typically don't have attachments via IMAP
230
+ end
231
+ end
232
+
233
+ # Legacy methods for backward compatibility
234
+ def original_attachments
235
+ postacert_attachments
236
+ end
237
+
171
238
  def original_regular_attachments
172
- original_attachments.reject(&:postacert?)
239
+ postacert_regular_attachments
173
240
  end
174
241
 
175
- # Get nested postacert.eml files from original message attachments
242
+ def original_body
243
+ postacert_body
244
+ end
245
+
246
+ def original_body_text
247
+ postacert_body_text
248
+ end
249
+
250
+ def original_body_html
251
+ postacert_body_html
252
+ end
253
+
254
+ # Get nested postacert.eml files from postacert message attachments
176
255
  def nested_postacerts
177
- @nested_postacerts ||= original_attachments.select(&:postacert?)
256
+ @nested_postacerts ||= postacert_attachments.select(&:postacert?)
178
257
  end
179
258
 
180
259
  # Check if original message has nested postacert.eml files
@@ -329,5 +408,76 @@ module PecRuby
329
408
  def extract_text_part(mail, preferred_type = "text/plain")
330
409
  NestedPostacertMessage.new(mail).send(:extract_text_part, mail, preferred_type)
331
410
  end
411
+
412
+ private
413
+
414
+ # Get the direct message body (without postacert.eml)
415
+ def direct_message_body
416
+ mail = direct_message_mail
417
+ return nil unless mail
418
+
419
+ text_part = extract_text_part(mail, "text/plain")
420
+ html_part = extract_text_part(mail, "text/html")
421
+
422
+ # Prefer text/plain, but return HTML if that's all we have
423
+ selected_part = text_part || html_part
424
+
425
+ return nil unless selected_part
426
+
427
+ raw_body = selected_part.body.decoded
428
+ charset = selected_part.charset ||
429
+ selected_part.content_type_parameters&.[]("charset") ||
430
+ "UTF-8"
431
+
432
+ content = raw_body.dup.force_encoding(charset).encode("UTF-8")
433
+
434
+ {
435
+ content: content,
436
+ content_type: selected_part.mime_type,
437
+ charset: charset
438
+ }
439
+ end
440
+
441
+ # Get the direct message body as plain text
442
+ def direct_message_body_text
443
+ mail = direct_message_mail
444
+ return nil unless mail
445
+
446
+ text_part = extract_text_part(mail, "text/plain")
447
+ return nil unless text_part
448
+
449
+ raw_body = text_part.body.decoded
450
+ charset = text_part.charset ||
451
+ text_part.content_type_parameters&.[]("charset") ||
452
+ "UTF-8"
453
+
454
+ raw_body.dup.force_encoding(charset).encode("UTF-8")
455
+ end
456
+
457
+ # Get the direct message body as HTML
458
+ def direct_message_body_html
459
+ mail = direct_message_mail
460
+ return nil unless mail
461
+
462
+ html_part = extract_text_part(mail, "text/html")
463
+ return nil unless html_part
464
+
465
+ raw_body = html_part.body.decoded
466
+ charset = html_part.charset ||
467
+ html_part.content_type_parameters&.[]("charset") ||
468
+ "UTF-8"
469
+
470
+ raw_body.dup.force_encoding(charset).encode("UTF-8")
471
+ end
472
+
473
+ # Get the direct message parsed as Mail object
474
+ def direct_message_mail
475
+ @direct_message_mail ||= begin
476
+ raw_data = @client.fetch_body_part(@uid, "")
477
+ Mail.read_from_string(raw_data)
478
+ rescue => e
479
+ raise Error, "Failed to extract direct message: #{e.message}"
480
+ end
481
+ end
332
482
  end
333
483
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PecRuby
4
- VERSION = "0.2.2"
5
- end
4
+ VERSION = '0.2.3'
5
+ end
data/lib/pec_ruby.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
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"
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
7
 
8
8
  # CLI is optional - only load if dependencies are available
9
9
  begin
10
- require_relative "pec_ruby/cli"
10
+ require_relative 'pec_ruby/cli'
11
11
  rescue LoadError
12
12
  # CLI dependencies not available - skip CLI functionality
13
13
  end
@@ -18,4 +18,5 @@ module PecRuby
18
18
  class AuthenticationError < Error; end
19
19
  class MessageNotFoundError < Error; end
20
20
  class PostacertNotFoundError < Error; end
21
- end
21
+ class FolderError < Error; end
22
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pec_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - EMG
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-13 00:00:00.000000000 Z
11
+ date: 2025-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-screen
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.8'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement