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 +4 -4
- data/CHANGELOG +16 -0
- data/lib/Imap/Message.rb +19 -15
- data/lib/Imap/Search.rb +12 -0
- data/lib/Imap/VERSION.rb +1 -1
- data/test/Imap/Message_test.rb +23 -0
- data/test/Imap/Search_test.rb +21 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a11cb6eb95eff27b2d107a0e1ad8fc727b1eabb6d2c24523ada41b3906b9d42e
|
|
4
|
+
data.tar.gz: 5a37e2eff7d0177f768aeb77182f04dd47d7686cdbd42f99814c35a140e4d270
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
7
|
-
# 3. Imap::Message.new(message_id, imap_client)
|
|
8
|
-
# 4. Imap::Message.new(message_id, imap_client).
|
|
9
|
-
# 5. Imap::Message.new(message_id, imap_client).
|
|
10
|
-
# 6. Imap::Message.new(message_id, imap_client).
|
|
11
|
-
# 7. Imap::Message.new(message_id, imap_client).
|
|
12
|
-
# 8. Imap::Message.new(message_id, imap_client).
|
|
13
|
-
# 9. Imap::Message.new(message_id, imap_client).
|
|
14
|
-
# 10. Imap::Message.new(message_id, imap_client).
|
|
15
|
-
# 11. Imap::Message.new(message_id, imap_client).
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
data/test/Imap/Message_test.rb
CHANGED
|
@@ -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')
|
data/test/Imap/Search_test.rb
CHANGED
|
@@ -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
|