imap 0.4.5 → 0.4.7

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: 78ec20b3a5517359526f4c5e0437a05b1ab97be444c1e1c741992f1ea595376e
4
- data.tar.gz: d5d2ac27eb982ff25264eaf947ee71981fba45cfc645b34dcce8148617a4c790
3
+ metadata.gz: 4b8f4ea427e7f3c30d6ce897ad13cdf4d22693b18170941d13c96f0e267bde83
4
+ data.tar.gz: 59a416b6e3687d94b3be00a9c2ce3c43090bd3ae09570b8cb0f9158f07d9a8a0
5
5
  SHA512:
6
- metadata.gz: 6a470cbf282ff1c1b84e6283b8e6a882262e02379eb03a50a65657346624667f576c5f14fecc1471ac94be2d6fe83bdca7f0d57ba76d65f613330662527c8e50
7
- data.tar.gz: 9754404cd62da6dfd26caa4e9c0ef3a7023704ff8f99da0864e57835a605b7c1dd04d4f992e35700184802ca1e6f075734ac89e3767234b1fd8cafb674f27315
6
+ metadata.gz: 16d311031d31ef2f8b50ebf349134429a3bd4a82174062ffbcf867369c23f039c444a099115ced05911f89093f5f30c3a0a4fe1014d45e1499beb6ecbf522fc4
7
+ data.tar.gz: '08dc54d7f1f9e867461b521f9383f9e6a56d3e747ee5ec697a53e7015c0f6df079fd2ec96cd8a677c3607bd824ec4e7cb4daefccde1acac09ad3265365457149'
data/CHANGELOG CHANGED
@@ -1,5 +1,22 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [0.4.7] - 2026-09-17, ~ Imap::Search: the chaining interface, which the header has documented and which has never run
4
+ - ~ Imap::Search: the generated methods named in lower case, so that from() answers where FROM() did. All 32 were upper case, being defined straight off the key constants, and none was reachable as the header wrote it.
5
+ - ~ Imap::Search: the generated methods return self, having returned the criteria hash, so that they chain.
6
+ - + Imap::Search#not: applies to what follows immediately and to nothing after it. + Negated, which carries a general key's value through the hash; a boolean needs none, false already reading as NOT.
7
+ - ~ Imap::Search#general_to_imap_search_key: ['NOT', key, value] where the value is Negated.
8
+ - ~ Imap::Search: the Usage header takes an imap_client, without which all() has nothing to search.
9
+ - + tests: the lower case names, that they chain, both negations, and that not applies once only.
10
+ - ~ Imap::VERSION: /0.4.6/0.4.7/
11
+
12
+ ## [0.4.6] - 2026-09-17, ~ Imap::Message#body: BODY.PEEK[TEXT]; ~ Imap::Search#to_imap_search_keys: raise upon a key which is neither
13
+ - ~ Imap::Message#body: BODY.PEEK[TEXT] for BODY[TEXT]. Reading a body set \Seen, so anything which looked at one marked it read; PEEK is the whole of the difference and the response still answers to BODY[TEXT].
14
+ - ~ Imap::Search#to_imap_search_keys: + UnknownSearchKey where the case matched neither operator. An unrecognised key was dropped and the search ran wider than it was asked for, saying nothing.
15
+ - ~ Imap::Message#to: [] where the envelope carries no to, which raised NoMethodError upon nil.
16
+ - ~ test/test_helper.rb: MockIMAP answers BODY.PEEK[TEXT] with BODY[TEXT] as a server does, records the attrs fetched, and gives message 99 an envelope with no to.
17
+ - + a test apiece: that #body peeks, that #to is empty where there is no to, and that an unknown key raises.
18
+ - ~ Imap::VERSION: /0.4.5/0.4.6/
19
+
3
20
  ## [0.4.5] - 2026-09-17, + Gemfile; ~ imap.gemspec: dependencies=, - spec.date, + metadata
4
21
  - + Gemfile: source and gemspec, as switches, eodhd and moby have it, so that the dependencies are declared once in the gemspec rather than restated here to drift. bitget and coinmarketcap restate theirs and are the minority. This was the only gem here with no Gemfile at all.
5
22
  - ~ imap.gemspec: + 'Gemfile' to spec.files, which every other gem here ships.
data/lib/Imap/Message.rb CHANGED
@@ -29,7 +29,7 @@ class Imap
29
29
  attr_accessor :message_id
30
30
 
31
31
  def body
32
- @body ||= fetch_data('BODY[TEXT]').first.attr['BODY[TEXT]']
32
+ @body ||= fetch_data('BODY.PEEK[TEXT]').first.attr['BODY[TEXT]']
33
33
  end
34
34
 
35
35
  def subject
@@ -41,7 +41,7 @@ class Imap
41
41
  end
42
42
 
43
43
  def to
44
- @to ||= fetch_data('ENVELOPE').first.attr['ENVELOPE'].to.map{|addr| addr.mailbox + '@' + addr.host}
44
+ @to ||= (fetch_data('ENVELOPE').first.attr['ENVELOPE'].to || []).map{|addr| addr.mailbox + '@' + addr.host}
45
45
  end
46
46
 
47
47
  def urls
data/lib/Imap/Search.rb CHANGED
@@ -2,8 +2,8 @@
2
2
  # Imap::Search
3
3
 
4
4
  # Usage:
5
- # 1. Imap::Search.new.not.subject('Payday Loans').answered.from('noreply@example.com').all
6
- # 2. Imap::Search.new({from: 'noreply@example.com', answered: true})
5
+ # 1. Imap::Search.new(imap_client).not.subject('Payday Loans').answered.from('noreply@example.com').all
6
+ # 2. Imap::Search.new(imap_client, {from: 'noreply@example.com', answered: true})
7
7
 
8
8
  # Notes:
9
9
  # 1. List of search keys taken from RFC-3501 (INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1), http://tools.ietf.org/html/rfc3501.
@@ -11,6 +11,13 @@
11
11
  class Imap
12
12
  class Search
13
13
 
14
+ class UnknownSearchKey < ArgumentError; end
15
+
16
+ # A general key carries its value; negating one has to carry both, the hash
17
+ # having nowhere else to put it. A boolean key needs none of this: false
18
+ # already reads as NOT.
19
+ Negated = Struct.new(:value)
20
+
14
21
  ALL_SEARCH_KEYS = %w{
15
22
  ALL
16
23
  ANSWERED
@@ -82,17 +89,25 @@ class Imap
82
89
  }
83
90
 
84
91
  BOOLEAN_SEARCH_KEYS.each do |boolean_search_key|
85
- define_method boolean_search_key do |value = true|
86
- criteria.merge!(boolean_search_key.to_sym => value)
92
+ define_method boolean_search_key.downcase do |value = true|
93
+ criteria.merge!(boolean_search_key.to_sym => negating? ? !value : value)
94
+ self
87
95
  end
88
96
  end
89
97
 
90
98
  GENERAL_SEARCH_KEYS.each do |general_search_key|
91
- define_method general_search_key do |value|
92
- criteria.merge!(general_search_key.to_sym => value)
99
+ define_method general_search_key.downcase do |value|
100
+ criteria.merge!(general_search_key.to_sym => negating? ? Negated.new(value) : value)
101
+ self
93
102
  end
94
103
  end
95
104
 
105
+ # Applies to what follows immediately, and to nothing after that.
106
+ def not
107
+ @negate = true
108
+ self
109
+ end
110
+
96
111
  attr_accessor :criteria
97
112
  attr_accessor :imap_client
98
113
 
@@ -112,12 +127,14 @@ class Imap
112
127
  m << general_to_imap_search_key(key, value)
113
128
  when boolean_operator?(key)
114
129
  m << boolean_to_imap_search_key(key, value)
130
+ else
131
+ raise UnknownSearchKey, "no such search key: #{key}"
115
132
  end
116
133
  end.flatten
117
134
  end
118
135
 
119
136
  def general_to_imap_search_key(key, value)
120
- [key, value]
137
+ value.is_a?(Negated) ? ['NOT', key, value.value] : [key, value]
121
138
  end
122
139
 
123
140
  def boolean_to_imap_search_key(key, value)
@@ -128,6 +145,12 @@ class Imap
128
145
  end
129
146
  end
130
147
 
148
+ def negating?
149
+ negate = @negate
150
+ @negate = false
151
+ negate
152
+ end
153
+
131
154
  def message_ids
132
155
  imap_client.imap.search(to_imap_search_keys)
133
156
  end
data/lib/Imap/VERSION.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Imap
2
- VERSION = '0.4.5'
2
+ VERSION = '0.4.7'
3
3
  end
@@ -40,6 +40,11 @@ describe Imap::Message do
40
40
  second_call = imap_message.body
41
41
  _(first_call.object_id).must_equal second_call.object_id
42
42
  end
43
+
44
+ it 'peeks, so that reading a body does not mark it seen' do
45
+ imap_message.body
46
+ _(client.imap.fetched_attrs).must_include 'BODY.PEEK[TEXT]'
47
+ end
43
48
  end
44
49
 
45
50
  describe '#subject' do
@@ -74,6 +79,10 @@ describe Imap::Message do
74
79
  addresses = imap_message.to
75
80
  _(addresses.first).must_equal('user@example.com')
76
81
  end
82
+
83
+ it 'is empty where the envelope carries no to' do
84
+ _(Imap::Message.new(99, client).to).must_equal []
85
+ end
77
86
  end
78
87
 
79
88
  describe '#urls' do
@@ -33,6 +33,10 @@ describe Imap::Search do
33
33
  _(keys).must_include 'SEEN'
34
34
  end
35
35
 
36
+ it 'raises upon a key which is neither' do
37
+ _{Imap::Search.new(client, bogus: 'x').to_imap_search_keys}.must_raise Imap::Search::UnknownSearchKey
38
+ end
39
+
36
40
  it 'handles multiple criteria' do
37
41
  test_search = Imap::Search.new(client, from: 'test@example.com', seen: true)
38
42
  keys = test_search.to_imap_search_keys
@@ -40,6 +44,30 @@ describe Imap::Search do
40
44
  end
41
45
  end
42
46
 
47
+ describe 'the chaining interface' do
48
+ it 'names the keys in lower case' do
49
+ _(Imap::Search.new(client).from('x@y.com').criteria).must_equal({FROM: 'x@y.com'})
50
+ end
51
+
52
+ it 'returns itself, so that it chains' do
53
+ search = Imap::Search.new(client)
54
+ _(search.from('x@y.com')).must_be_same_as search
55
+ end
56
+
57
+ it 'negates a general key' do
58
+ _(Imap::Search.new(client).not.subject('Payday Loans').to_imap_search_keys).must_equal ['NOT', 'SUBJECT', 'Payday Loans']
59
+ end
60
+
61
+ it 'negates a boolean key' do
62
+ _(Imap::Search.new(client).not.seen.to_imap_search_keys).must_equal ['NOT', 'SEEN']
63
+ end
64
+
65
+ it 'negates what follows immediately and nothing after it' do
66
+ keys = Imap::Search.new(client).not.subject('Payday Loans').answered.from('noreply@example.com').to_imap_search_keys
67
+ _(keys).must_equal ['NOT', 'SUBJECT', 'Payday Loans', 'ANSWERED', 'FROM', 'noreply@example.com']
68
+ end
69
+ end
70
+
43
71
  describe '#message_ids' do
44
72
  it 'returns an array of message IDs' do
45
73
  ids = search.message_ids
data/test/test_helper.rb CHANGED
@@ -31,7 +31,10 @@ class MockIMAP
31
31
  [1, 2, 3]
32
32
  end
33
33
 
34
+ attr_reader :fetched_attrs
35
+
34
36
  def fetch(message_id, attrs)
37
+ @fetched_attrs = attrs
35
38
  [OpenStruct.new(attr: mock_fetch_attrs(message_id, attrs))]
36
39
  end
37
40
 
@@ -58,7 +61,7 @@ class MockIMAP
58
61
  result = {}
59
62
  attrs.each do |attr|
60
63
  case attr
61
- when 'BODY[TEXT]'
64
+ when 'BODY[TEXT]', 'BODY.PEEK[TEXT]'
62
65
  result['BODY[TEXT]'] = "Mock body for message #{message_id}"
63
66
  when 'BODY[HEADER.FIELDS (SUBJECT)]'
64
67
  result['BODY[HEADER.FIELDS (SUBJECT)]'] = "Subject: Mock Subject #{message_id}\r\n"
@@ -66,7 +69,7 @@ class MockIMAP
66
69
  result['BODY[HEADER.FIELDS (FROM)]'] = "From: sender@example.com\r\n"
67
70
  when 'ENVELOPE'
68
71
  result['ENVELOPE'] = OpenStruct.new(
69
- to: [OpenStruct.new(mailbox: 'user', host: 'example.com')]
72
+ to: message_id == 99 ? nil : [OpenStruct.new(mailbox: 'user', host: 'example.com')]
70
73
  )
71
74
  end
72
75
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -85,7 +85,7 @@ files:
85
85
  - test/Imap/Message_test.rb
86
86
  - test/Imap/Search_test.rb
87
87
  - test/Imap/VERSION_test.rb
88
- - test/imap_test.rb
88
+ - test/Imap_test.rb
89
89
  - test/test_helper.rb
90
90
  homepage: https://github.com/thoran/imap
91
91
  licenses:
File without changes