imap.rb 0.5.0 → 0.6.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 +10 -0
- data/lib/Imap/Message.rb +26 -5
- data/lib/Imap/VERSION.rb +1 -1
- data/test/Imap/Message_test.rb +24 -0
- data/test/test_helper.rb +8 -6
- 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: db42d933da16aca65af3c854ea0f26da5ec173da7df4260e0bb2879476da0bbd
|
|
4
|
+
data.tar.gz: 1c751f2b12f3dcaa5cfa6035ead2c6b24ba81c3c866cd7bf454e56f67ada69ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fea3b0f91b2813bc83be27ee944d2d34c4e7b7740b406349e85e5d24fbccc5b199a0b272e144bf8e3ea096a5a9dfa68760aef3c537cd6ddaf92f9533a9ee209c
|
|
7
|
+
data.tar.gz: 6e8c310a8660730d3c268f17f236ea92c7d33bd3de96edfb189acb4c0f34dfd6a7d9bc7037f8cb38486ac34836ba04c99df1960649f755a93c097cfb8ced3c00
|
data/CHANGELOG
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## 20260917
|
|
4
4
|
|
|
5
|
+
0.6.0: One fetch for a slice of messages rather than one per attribute per message.
|
|
6
|
+
|
|
7
|
+
1. ~ Imap::Message.search: fetches the ENVELOPE for a slice of message ids and hands each Imap::Message its own, + SLICE = 200. A hundred messages cost a round trip apiece for the subject and another apiece for the from; they cost one for the hundred now, which at any real latency is the whole cost of a search.
|
|
8
|
+
2. ~ Imap::Message#initialize: + attrs, which search() supplies and a caller need not. + Imap::Message#envelope, which reads what was prefetched or fetches where there is none, so a message built by hand behaves as before.
|
|
9
|
+
3. ~ Imap::Message#subject, #from, #to: read from the envelope rather than fetching a header apiece. #from answers 'name <mailbox@host>' where the envelope names one, and the bare address where it does not, having answered the raw From header.
|
|
10
|
+
4. ~ Imap::Message#body: unchanged and still lazy, a body being the one thing worth not fetching until it is wanted.
|
|
11
|
+
5. ~ test/test_helper.rb: MockIMAP#fetch takes a list as a server does, answers with a seqno apiece, and counts its calls. Its envelope carries a subject and a from; the two BODY[HEADER.FIELDS] cases went with the code that asked for them.
|
|
12
|
+
6. + tests: that a search of three fetches once, that the prefetched envelope is not fetched again, and that a message built by hand still fetches.
|
|
13
|
+
7. ~ Imap::VERSION: /0.5.0/0.6.0/
|
|
14
|
+
|
|
5
15
|
0.5.0: + OR and NOT to the criteria, and HEADER given its field
|
|
6
16
|
|
|
7
17
|
1. + Imap::Search#any_of: or: [{from: 'a@x'}, {to: 'b@y'}]. OR takes two keys and no more, so three or more nest, OR OR a b c, as the server reads them. Each side is a criteria hash of its own and may nest further.
|
data/lib/Imap/Message.rb
CHANGED
|
@@ -15,11 +15,20 @@ require_relative './Search'
|
|
|
15
15
|
|
|
16
16
|
class Imap
|
|
17
17
|
class Message
|
|
18
|
+
|
|
19
|
+
SLICE = 200
|
|
18
20
|
class << self
|
|
19
21
|
|
|
22
|
+
# One fetch for a slice of messages rather than one per attribute per
|
|
23
|
+
# message. A hundred messages cost a round trip apiece for the subject and
|
|
24
|
+
# another apiece for the from; they cost one for the hundred now.
|
|
20
25
|
def search(imap_client, **search_criteria)
|
|
21
26
|
message_ids = Imap::Search.new(imap_client, search_criteria).message_ids
|
|
22
|
-
message_ids.
|
|
27
|
+
message_ids.each_slice(SLICE).flat_map do |slice|
|
|
28
|
+
imap_client.imap.fetch(slice, ['ENVELOPE']).collect do |data|
|
|
29
|
+
Imap::Message.new(data.seqno, imap_client, data.attr)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
23
32
|
end
|
|
24
33
|
alias_method :find, :search
|
|
25
34
|
|
|
@@ -33,15 +42,15 @@ class Imap
|
|
|
33
42
|
end
|
|
34
43
|
|
|
35
44
|
def subject
|
|
36
|
-
@subject ||=
|
|
45
|
+
@subject ||= envelope.subject.to_s.strip
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
def from
|
|
40
|
-
@from ||=
|
|
49
|
+
@from ||= address_to_s(envelope.from && envelope.from.first)
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
def to
|
|
44
|
-
@to ||= (
|
|
53
|
+
@to ||= (envelope.to || []).collect{|address| address_to_s(address)}
|
|
45
54
|
end
|
|
46
55
|
|
|
47
56
|
def urls
|
|
@@ -55,9 +64,21 @@ class Imap
|
|
|
55
64
|
|
|
56
65
|
private
|
|
57
66
|
|
|
58
|
-
def initialize(message_id = nil, imap_client = nil)
|
|
67
|
+
def initialize(message_id = nil, imap_client = nil, attrs = nil)
|
|
59
68
|
@message_id = message_id
|
|
60
69
|
@imap_client = imap_client
|
|
70
|
+
@attrs = attrs
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Prefetched where search() built this, fetched where the caller built it.
|
|
74
|
+
def envelope
|
|
75
|
+
@envelope ||= (@attrs && @attrs['ENVELOPE']) || fetch_data('ENVELOPE').first.attr['ENVELOPE']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def address_to_s(address)
|
|
79
|
+
return nil unless address
|
|
80
|
+
email = [address.mailbox, address.host].compact.join('@')
|
|
81
|
+
address.name.to_s.empty? ? email : "#{address.name} <#{email}>"
|
|
61
82
|
end
|
|
62
83
|
|
|
63
84
|
def fetch_data(*attrs)
|
data/lib/Imap/VERSION.rb
CHANGED
data/test/Imap/Message_test.rb
CHANGED
|
@@ -30,6 +30,30 @@ describe Imap::Message do
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
describe 'the batched fetch' do
|
|
34
|
+
it 'fetches once for the whole slice rather than once per message' do
|
|
35
|
+
messages = Imap::Message.search(client, from: 'test@example.com')
|
|
36
|
+
_(messages.size).must_equal 3
|
|
37
|
+
_(client.imap.fetch_count).must_equal 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'reads the prefetched envelope rather than fetching again' do
|
|
41
|
+
message = Imap::Message.search(client, from: 'test@example.com').first
|
|
42
|
+
before = client.imap.fetch_count
|
|
43
|
+
message.subject
|
|
44
|
+
message.from
|
|
45
|
+
message.to
|
|
46
|
+
_(client.imap.fetch_count).must_equal before
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'still fetches for a message the caller built' do
|
|
50
|
+
message = Imap::Message.new(1, client)
|
|
51
|
+
before = client.imap.fetch_count.to_i
|
|
52
|
+
message.subject
|
|
53
|
+
_(client.imap.fetch_count).must_equal before + 1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
33
57
|
describe '#body' do
|
|
34
58
|
it 'returns the message body' do
|
|
35
59
|
_(imap_message.body).must_match(/Mock body/)
|
data/test/test_helper.rb
CHANGED
|
@@ -32,10 +32,14 @@ class MockIMAP
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
attr_reader :fetched_attrs
|
|
35
|
+
attr_reader :fetch_count
|
|
35
36
|
|
|
36
|
-
def fetch(
|
|
37
|
+
def fetch(message_ids, attrs)
|
|
37
38
|
@fetched_attrs = attrs
|
|
38
|
-
|
|
39
|
+
@fetch_count = (@fetch_count || 0) + 1
|
|
40
|
+
Array(message_ids).collect do |message_id|
|
|
41
|
+
OpenStruct.new(seqno: message_id, attr: mock_fetch_attrs(message_id, attrs))
|
|
42
|
+
end
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def store(message_id, flags, values); end
|
|
@@ -63,12 +67,10 @@ class MockIMAP
|
|
|
63
67
|
case attr
|
|
64
68
|
when 'BODY[TEXT]', 'BODY.PEEK[TEXT]'
|
|
65
69
|
result['BODY[TEXT]'] = "Mock body for message #{message_id}"
|
|
66
|
-
when 'BODY[HEADER.FIELDS (SUBJECT)]'
|
|
67
|
-
result['BODY[HEADER.FIELDS (SUBJECT)]'] = "Subject: Mock Subject #{message_id}\r\n"
|
|
68
|
-
when 'BODY[HEADER.FIELDS (FROM)]'
|
|
69
|
-
result['BODY[HEADER.FIELDS (FROM)]'] = "From: sender@example.com\r\n"
|
|
70
70
|
when 'ENVELOPE'
|
|
71
71
|
result['ENVELOPE'] = OpenStruct.new(
|
|
72
|
+
subject: "Mock Subject #{message_id}",
|
|
73
|
+
from: [OpenStruct.new(mailbox: 'sender', host: 'example.com')],
|
|
72
74
|
to: message_id == 99 ? nil : [OpenStruct.new(mailbox: 'user', host: 'example.com')]
|
|
73
75
|
)
|
|
74
76
|
end
|