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 +4 -4
- data/CHANGELOG +17 -0
- data/lib/Imap/Message.rb +2 -2
- data/lib/Imap/Search.rb +30 -7
- data/lib/Imap/VERSION.rb +1 -1
- data/test/Imap/Message_test.rb +9 -0
- data/test/Imap/Search_test.rb +28 -0
- data/test/test_helper.rb +5 -2
- metadata +2 -2
- /data/test/{imap_test.rb → Imap_test.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b8f4ea427e7f3c30d6ce897ad13cdf4d22693b18170941d13c96f0e267bde83
|
|
4
|
+
data.tar.gz: 59a416b6e3687d94b3be00a9c2ce3c43090bd3ae09570b8cb0f9158f07d9a8a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/test/Imap/Message_test.rb
CHANGED
|
@@ -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
|
data/test/Imap/Search_test.rb
CHANGED
|
@@ -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.
|
|
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/
|
|
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
|