imap.rb 0.8.0 → 0.10.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: 45cf03dcb336e9795a5ef5339e6ac0c6e3b2f5e79014f3462684f39f79aae98f
4
- data.tar.gz: e204d83b456b966f0f9a9c3a55acfb492564da65a2b2be22d8c27326104ba755
3
+ metadata.gz: 68b300810859bc9e1aad02afe6eb9ae614581c114c05b175e59bffea2e94b4cd
4
+ data.tar.gz: fd607d6ddca5bf9f9993f5fcc29a56c658671ada38d056c9bb9f6ce0c65d3968
5
5
  SHA512:
6
- metadata.gz: 20938befdd15cc9da7f5679b0043facbdbc0d3a69b04f93a0100c43b4314b6832ecfed375484d1429221ee60703dfc94ac3abdd1f4ff40060ed528828187fcf4
7
- data.tar.gz: e467e3aed0b2b09cc86e175a1b3f940e37922232e4efb97998c5dc7a959b5a3f9b8f012bb1a8ff6505d7dff83f488e418c5e7d8670fe5b4db7333991ff683d77
6
+ metadata.gz: 4864d678e3d41d3ad94a001ebb08460c1f93d2441c5eeea117fffc859609de853ee6e833cb1fb46f285b5a9b3f13427e04b483754fbd00504cb71d6ef38f54fd
7
+ data.tar.gz: 87db28691770c3bd7885866e25f5507f86785b9accf9689099cc3423ef8f82873e9be8a86145c53084566ef49565776e18d5337e683d71189e35a12a21e0f540
data/CHANGELOG CHANGED
@@ -1,7 +1,27 @@
1
1
  # imap/CHANGELOG
2
2
 
3
+ ## 20260919
4
+
5
+ 0.10.0: ~ Imap.setup: + Imap::LoginFailed; ~ the gemspecs: /'~> 0.4'/'>= 0.4.5'/ upon net-imap. Imap.setup ignored the false which Imap#login returns when the server rejects the password, and handed back a client which was connected but not logged in.
6
+
7
+ 1. + Imap::LoginFailed
8
+ 2. + login!(): raises where the server rejects the password, and where only one of the username and the password is given.
9
+ 3. ~ Imap.setup: login!() for #login, and /&&/||/ upon the credentials.
10
+ 4. ~ imap.gemspec, imap.rb.gemspec: /'~> 0.4'/'>= 0.4.5'/ upon net-imap. FetchData#uid, #flags, #internaldate, #bodystructure and #message are absent in 0.4.0 and present in 0.4.5, and 0.7.0 reads all five. No ceiling, nothing being known to break at 1.0.
11
+ 5. + 4 tests: .setup upon a rejection, that it does not go on to select, a half-given pair, and neither given.
12
+ 6. ~ Imap::VERSION: /0.9.0/0.10.0/
13
+
3
14
  ## 20260917
4
15
 
16
+ 0.9.0: + all_of and ALL
17
+
18
+ 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.
19
+ 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.
20
+ 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.
21
+ 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'].
22
+ 5. ~ Imap::VERSION: /0.8.0/0.9.0/
23
+
24
+
5
25
  0.8.0: + Imap::Message.for, the fetch without the search, and a class a program can subclass.
6
26
 
7
27
  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.
data/imap.rb.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.required_ruby_version = '>= 2.7'
29
29
 
30
30
  spec.dependencies = [
31
- ['net-imap', '~> 0.4']
31
+ ['net-imap', '>= 0.4.5']
32
32
  ]
33
33
 
34
34
  spec.development_dependencies = [
data/lib/Imap/Message.rb CHANGED
@@ -29,7 +29,6 @@ class Imap
29
29
  ENCODED_WORD = /=\?([^?]+)\?([BbQq])\?([^?]*)\?=/
30
30
 
31
31
  class << self
32
-
33
32
  def search(imap_client, **search_criteria)
34
33
  self.for(imap_client, Imap::Search.new(imap_client, search_criteria).message_ids)
35
34
  end
@@ -68,7 +67,6 @@ class Imap
68
67
  rescue StandardError
69
68
  text
70
69
  end
71
-
72
70
  end # class << self
73
71
 
74
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.8.0'
2
+ VERSION = '0.10.0'
3
3
  end
data/lib/imap.rb CHANGED
@@ -6,18 +6,31 @@ require_relative './Imap/Message'
6
6
  require_relative './Imap/VERSION'
7
7
 
8
8
  class Imap
9
+ class LoginFailed < StandardError; end
10
+
9
11
  class << self
10
12
 
11
13
  def setup(config)
12
14
  raise ArgumentError, "Server must be specified" unless config[:server]
13
15
  imap_client = Imap.new(**config)
14
- if config[:username] && config[:password]
15
- imap_client.login(username: config[:username], password: config[:password])
16
+ login!(imap_client, config) if config[:username] || config[:password]
17
+ imap_client.mailbox = config[:mailbox] if config[:mailbox]
18
+ imap_client
19
+ end
20
+
21
+ private
22
+
23
+ # A half-given pair is a mistake rather than a reason not to log in, and a
24
+ # refusal is not something to carry on from: what follows is a connection
25
+ # which answers BAD to whatever is asked of it, several commands away from
26
+ # the fault.
27
+ def login!(imap_client, config)
28
+ unless config[:username] && config[:password]
29
+ raise LoginFailed, "#{config[:server]} wants both a username and a password; only the #{config[:username] ? 'username' : 'password'} was given"
16
30
  end
17
- if config[:mailbox]
18
- imap_client.mailbox = config[:mailbox]
31
+ unless imap_client.login(username: config[:username], password: config[:password])
32
+ raise LoginFailed, "#{config[:server]} refused #{config[:username]}"
19
33
  end
20
- imap_client
21
34
  end
22
35
 
23
36
  end # class << self
@@ -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
data/test/Imap_test.rb CHANGED
@@ -36,6 +36,47 @@ describe Imap do
36
36
  end
37
37
  end
38
38
 
39
+ describe ".setup, upon a login which does not take" do
40
+ def refusing_mock
41
+ mock = MockIMAP.new('imap.example.com', ssl: true)
42
+ def mock.login(username, password)
43
+ raise Net::IMAP::NoResponseError, OpenStruct.new(data: OpenStruct.new(text: 'Authentication failed'))
44
+ end
45
+ mock
46
+ end
47
+
48
+ it 'raises rather than handing back a connection which is not logged in' do
49
+ Net::IMAP.stub(:new, refusing_mock) do
50
+ error = _(proc{Imap.setup(server: 'imap.example.com', username: 'user', password: 'wrong')}).must_raise(Imap::LoginFailed)
51
+ _(error.message).must_equal 'imap.example.com refused user'
52
+ end
53
+ end
54
+
55
+ it 'does not go on to select a mailbox' do
56
+ mock = refusing_mock
57
+ Net::IMAP.stub(:new, mock) do
58
+ _(proc{Imap.setup(server: 'imap.example.com', username: 'user', password: 'wrong', mailbox: 'INBOX')}).must_raise(Imap::LoginFailed)
59
+ end
60
+ _(mock.select_called).must_equal false
61
+ end
62
+
63
+ it 'raises where only one of the pair is given, rather than skipping the login' do
64
+ Net::IMAP.stub(:new, MockIMAP.new('imap.example.com', ssl: true)) do
65
+ error = _(proc{Imap.setup(server: 'imap.example.com', username: 'user')}).must_raise(Imap::LoginFailed)
66
+ _(error.message).must_match(/only the username was given/)
67
+ _(proc{Imap.setup(server: 'imap.example.com', password: 'secret')}).must_raise(Imap::LoginFailed)
68
+ end
69
+ end
70
+
71
+ it 'does not log in at all where neither is given' do
72
+ mock = MockIMAP.new('imap.example.com', ssl: true)
73
+ Net::IMAP.stub(:new, mock) do
74
+ _(Imap.setup(server: 'imap.example.com')).must_be_instance_of Imap
75
+ end
76
+ _(mock.login_called).must_equal false
77
+ end
78
+ end
79
+
39
80
  describe "#login" do
40
81
  it 'returns true on successful login' do
41
82
  Net::IMAP.stub(:new, MockIMAP.new('imap.example.com', ssl: true)) do
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.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -13,16 +13,16 @@ dependencies:
13
13
  name: net-imap
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0.4'
18
+ version: 0.4.5
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '0.4'
25
+ version: 0.4.5
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: minitest
28
28
  requirement: !ruby/object:Gem::Requirement