imap.rb 0.7.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9f5b0c607f3b3e66d7284613916d45c077de9a0e97f50f960fa8e63fd7b5586
4
- data.tar.gz: 4529940cbd85a56bfbd4ef9d472536234852cd4cabf6349f90763644905ab953
3
+ metadata.gz: a11cb6eb95eff27b2d107a0e1ad8fc727b1eabb6d2c24523ada41b3906b9d42e
4
+ data.tar.gz: 5a37e2eff7d0177f768aeb77182f04dd47d7686cdbd42f99814c35a140e4d270
5
5
  SHA512:
6
- metadata.gz: 7279ae1bb4fd2456fa2acf57cad565a0af28d2be5795e1e6bf9924b2b9f6583acf2e4a71735944c8bf3f3dc56d403d3abba5943eb87ea15054f12c718665bab9
7
- data.tar.gz: 060c6a1b974e983979130cf62d03e0630ef548b64b9e80a522c6064c031126385de84f762cb1a158fd50b03711902f1430038b8aa0b3f63864c7dc4e85366548
6
+ metadata.gz: 8a22fc1e24e9c7765d06733837547a29f581083586b0e9fdef601756ee1d61561b44689cb7dcf5ed62f3db8fc2096ac295ac8859fc765de30fd1135123a9aac6
7
+ data.tar.gz: 22a69f98811483319992902f342267743bbb69acac1723b1dc8067a54f9805e8965d91925a289cfd376eac22fff3d8ec333125f4f8733cf05debd9025ad38843
data/CHANGELOG CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## 20260917
4
4
 
5
+ 0.9.0: + all_of and ALL
6
+
7
+ 1. + Imap::Search#all_of: all_of: [{text: 'invoice'}, {text: 'overdue'}] for TEXT invoice TEXT overdue. IMAP has no AND keyword, keys simply sitting side by side, but a hash cannot hold the same key twice, so two terms upon the one key could not be asked for at all: {text: 'a', text: 'b'} keeps b and drops a without a word. Each element is a criteria hash of its own, so or and not nest inside it.
8
+ 2. ~ Imap::Search#to_imap_search_keys: + ALL_OF and + ALL, read before the operator tests beside OR and NOT, being none of the three. ALL sat in ALL_SEARCH_KEYS and in neither operator list, so {all: true} raised UnknownSearchKey. It is the key meaning every message, which is precisely what a search with no criteria has to send, so the hash could not express the simplest search there is.
9
+ 3. ALL is a case and not a member of BOOLEAN_SEARCH_KEYS, where it appears to belong: each of those defines a method of its own name, and #all is already the alias for #message_ids which the header's own example ends upon. Adding it there redefined the alias silently, which warning = true reported and 0.4.4 turned on for this.
10
+ 4. + 5 tests: that all_of ands a list, that it is how the one key carries several terms where a hash cannot, that or and not nest within it, that it sits beside the plain keys, and that {all: true} answers ['ALL'].
11
+ 5. ~ Imap::VERSION: /0.8.0/0.9.0/
12
+
13
+
14
+ 0.8.0: + Imap::Message.for, the fetch without the search, and a class a program can subclass.
15
+
16
+ 1. + Imap::Message.for(imap_client, message_ids): the batched fetch on its own, .search now being the search and then this. A program which builds its own search keys had no way in: .search couples the two, so search-mail had 159 lines of its own Hit reimplementing the fetch beside it.
17
+ 2. ~ Imap::Message.for: builds with new rather than Imap::Message.new, so that a subclass is handed back its own kind. A program which wants a message of its own, with a mailbox or a decoded body upon it, subclasses rather than starts again.
18
+ 3. + 4 tests: that .for builds for ids searched elsewhere, that it fetches once, that it is empty where nothing was found, and that a subclass gets itself back.
19
+ 4. ~ Imap::VERSION: /0.7.0/0.8.0/
20
+
5
21
  0.7.0: ~ Imap::Message: + #uid, #flags, #seen?, #size, #received_at, #sent_at, #cc, #from_address, #from_domain, #attachment?, #attachment_filenames, #source, .decode; + Imap#mailboxes
6
22
 
7
23
  1. + Imap::Message#uid, #flags, #seen?, #size, #received_at, #sent_at, #cc, #from_address, #from_domain, #attachment?, #attachment_filenames, #source. Everything a search had to go around the gem to Net::IMAP for.
data/lib/Imap/Message.rb CHANGED
@@ -3,16 +3,17 @@
3
3
 
4
4
  # Usage:
5
5
  # 1. Imap::Message.search(imap_client, {from: 'noreply@example.com'})
6
- # 2. Imap::Message.new(message_id, imap_client)
7
- # 3. Imap::Message.new(message_id, imap_client).body
8
- # 4. Imap::Message.new(message_id, imap_client).urls
9
- # 5. Imap::Message.new(message_id, imap_client).mark_as_read
10
- # 6. Imap::Message.new(message_id, imap_client).subject
11
- # 7. Imap::Message.new(message_id, imap_client).from
12
- # 8. Imap::Message.new(message_id, imap_client).to
13
- # 9. Imap::Message.new(message_id, imap_client).received_at
14
- # 10. Imap::Message.new(message_id, imap_client).attachment_filenames
15
- # 11. Imap::Message.new(message_id, imap_client).source
6
+ # 2. Imap::Message.for(imap_client, imap_client.imap.search(['ALL']))
7
+ # 3. Imap::Message.new(message_id, imap_client)
8
+ # 4. Imap::Message.new(message_id, imap_client).body
9
+ # 5. Imap::Message.new(message_id, imap_client).urls
10
+ # 6. Imap::Message.new(message_id, imap_client).mark_as_read
11
+ # 7. Imap::Message.new(message_id, imap_client).subject
12
+ # 8. Imap::Message.new(message_id, imap_client).from
13
+ # 9. Imap::Message.new(message_id, imap_client).to
14
+ # 10. Imap::Message.new(message_id, imap_client).received_at
15
+ # 11. Imap::Message.new(message_id, imap_client).attachment_filenames
16
+ # 12. Imap::Message.new(message_id, imap_client).source
16
17
 
17
18
  require 'time'
18
19
 
@@ -28,19 +29,23 @@ class Imap
28
29
  ENCODED_WORD = /=\?([^?]+)\?([BbQq])\?([^?]*)\?=/
29
30
 
30
31
  class << self
32
+ def search(imap_client, **search_criteria)
33
+ self.for(imap_client, Imap::Search.new(imap_client, search_criteria).message_ids)
34
+ end
35
+ alias_method :find, :search
31
36
 
32
37
  # One fetch for a slice of messages rather than one per attribute per
33
38
  # message. A hundred messages cost a round trip apiece for the subject and
34
39
  # another apiece for the from; they cost one for the hundred now.
35
- def search(imap_client, **search_criteria)
36
- message_ids = Imap::Search.new(imap_client, search_criteria).message_ids
40
+ #
41
+ # new and not Imap::Message.new, so that a subclass gets its own kind back.
42
+ def for(imap_client, message_ids)
37
43
  message_ids.each_slice(SLICE).flat_map do |slice|
38
44
  imap_client.imap.fetch(slice, FETCH_ATTRIBUTES).collect do |data|
39
- Imap::Message.new(data.seqno, imap_client, data)
45
+ new(data.seqno, imap_client, data)
40
46
  end
41
47
  end
42
48
  end
43
- alias_method :find, :search
44
49
 
45
50
  # RFC 2047: =?charset?B?base64?= and =?charset?Q?quoted-printable?=, which
46
51
  # headers carry wherever the subject or a name is not ASCII.
@@ -62,7 +67,6 @@ class Imap
62
67
  rescue StandardError
63
68
  text
64
69
  end
65
-
66
70
  end # class << self
67
71
 
68
72
  attr_accessor :imap_client
data/lib/Imap/Search.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  # Usage:
5
5
  # 1. Imap::Search.new(imap_client).not.subject('Payday Loans').answered.from('noreply@example.com').all
6
6
  # 2. Imap::Search.new(imap_client, {from: 'noreply@example.com', answered: true})
7
+ # 3. Imap::Search.new(imap_client, {all_of: [{text: 'invoice'}, {text: 'overdue'}]})
7
8
 
8
9
  # Notes:
9
10
  # 1. List of search keys taken from RFC-3501 (INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1), http://tools.ietf.org/html/rfc3501.
@@ -123,6 +124,10 @@ class Imap
123
124
  criteria.inject([]) do |m, kv|
124
125
  key, value = kv.first.to_s.upcase, kv.last
125
126
  case
127
+ when key == 'ALL'
128
+ m << 'ALL'
129
+ when key == 'ALL_OF'
130
+ m << all_of(value)
126
131
  when key == 'OR'
127
132
  m << any_of(value)
128
133
  when key == 'NOT'
@@ -137,6 +142,13 @@ class Imap
137
142
  end.flatten
138
143
  end
139
144
 
145
+ # AND has no keyword: keys sit side by side and the server ands them. A hash
146
+ # cannot hold the same key twice, though, so several terms upon the one key
147
+ # say so here: all_of: [{text: 'a'}, {text: 'b'}] for TEXT a TEXT b.
148
+ def all_of(criteria_hashes)
149
+ criteria_hashes.flat_map{|criteria_hash| Search.new(nil, criteria_hash).to_imap_search_keys}
150
+ end
151
+
140
152
  # OR takes two keys and no more, so three criteria nest: OR OR a b c. Each
141
153
  # is a criteria hash of its own, so either side may nest further.
142
154
  def any_of(criteria_hashes)
data/lib/Imap/VERSION.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Imap
2
- VERSION = '0.7.0'
2
+ VERSION = '0.9.0'
3
3
  end
@@ -30,6 +30,29 @@ describe Imap::Message do
30
30
  end
31
31
  end
32
32
 
33
+ describe '.for' do
34
+ it 'builds messages for ids the caller searched for itself' do
35
+ messages = Imap::Message.for(client, [1, 2, 3])
36
+ _(messages.size).must_equal 3
37
+ _(messages.first).must_be_instance_of Imap::Message
38
+ _(messages.first.subject).must_match(/Mock Subject/)
39
+ end
40
+
41
+ it 'fetches once for the whole slice' do
42
+ Imap::Message.for(client, [1, 2, 3])
43
+ _(client.imap.fetch_count).must_equal 1
44
+ end
45
+
46
+ it 'is empty where nothing was found' do
47
+ _(Imap::Message.for(client, [])).must_equal []
48
+ end
49
+
50
+ it 'gives a subclass back its own kind' do
51
+ hit = Class.new(Imap::Message)
52
+ _(hit.for(client, [1]).first).must_be_instance_of hit
53
+ end
54
+ end
55
+
33
56
  describe 'the batched fetch' do
34
57
  it 'fetches once for the whole slice rather than once per message' do
35
58
  messages = Imap::Message.search(client, from: 'test@example.com')
@@ -47,6 +47,27 @@ describe Imap::Search do
47
47
  describe 'OR, NOT and HEADER' do
48
48
  def keys(criteria) = Imap::Search.new(client, criteria).to_imap_search_keys
49
49
 
50
+ it 'answers ALL, which every key list named and neither operator list held' do
51
+ _(keys(all: true)).must_equal ['ALL']
52
+ end
53
+
54
+ it 'ands a list of criteria hashes for ALL_OF' do
55
+ _(keys(all_of: [{text: 'invoice'}, {text: 'overdue'}])).must_equal ['TEXT', 'invoice', 'TEXT', 'overdue']
56
+ end
57
+
58
+ it 'is how the one key carries more than one term, a hash holding it once' do
59
+ _(keys({text: 'invoice'}.merge(text: 'overdue'))).must_equal ['TEXT', 'overdue']
60
+ _(keys(all_of: [{text: 'invoice'}, {text: 'overdue'}])).must_equal ['TEXT', 'invoice', 'TEXT', 'overdue']
61
+ end
62
+
63
+ it 'nests OR and NOT inside ALL_OF' do
64
+ _(keys(all_of: [{not: {text: 'draft'}}, {or: [{from: 'a@x'}, {to: 'b@y'}]}])).must_equal ['NOT', 'TEXT', 'draft', 'OR', 'FROM', 'a@x', 'TO', 'b@y']
65
+ end
66
+
67
+ it 'sits beside the plain keys' do
68
+ _(keys(since: '1-Sep-2026', all_of: [{text: 'a'}, {text: 'b'}])).must_equal ['SINCE', '1-Sep-2026', 'TEXT', 'a', 'TEXT', 'b']
69
+ end
70
+
50
71
  it 'takes two criteria hashes for OR' do
51
72
  _(keys(or: [{from: 'a@x'}, {to: 'b@y'}])).must_equal ['OR', 'FROM', 'a@x', 'TO', 'b@y']
52
73
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran