imap.rb 0.4.9
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 +7 -0
- data/CHANGELOG +93 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +77 -0
- data/Rakefile +12 -0
- data/imap.rb.gemspec +58 -0
- data/lib/Imap/Message.rb +67 -0
- data/lib/Imap/Search.rb +166 -0
- data/lib/Imap/VERSION.rb +3 -0
- data/lib/imap.rb +83 -0
- data/test/Imap/Message_test.rb +121 -0
- data/test/Imap/Search_test.rb +120 -0
- data/test/Imap/VERSION_test.rb +15 -0
- data/test/Imap_test.rb +119 -0
- data/test/test_helper.rb +78 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b062efdc696b59573e564f135485c3e987a8a8078b36e87783537cbcd040712a
|
|
4
|
+
data.tar.gz: 150f927c8023ed6cbbb1b58fa5b863620ba2043ec71058c52f36a75c817e9e0a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c979d4b09d86154e500454440396443d9db9ee8134603ea5d432ed093e46bc3a54fa33aaa24667302dc540566e8863c6d50c22c112c87ee91a1b224c7a4d567c
|
|
7
|
+
data.tar.gz: 4e91af7317d63811a9ec9dff17ff14130926bf101d4631e6fbfa5069501c12e90b1af0fa28efa315a6a1dc52b823b012e89f14e6d7dbdec3b8b7fe1fa44e3bff
|
data/CHANGELOG
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# imap/CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 20260917
|
|
4
|
+
|
|
5
|
+
0.4.9: + imap.rb.gemspec, so that the gem resolves under both names.
|
|
6
|
+
|
|
7
|
+
1. + imap.rb.gemspec: the same code published as imap.rb, which is how gems are named here: 21 carry the suffix and imap is one of the few which do not. rubygems has no notion of an alias, so the two are separate gems which must be kept at the same version by hand.
|
|
8
|
+
2. It differs from imap.gemspec in the three lines measurand and namo differ in, the heading comment naming the file, the name, and the gemspec each lists among its own files. Neither ships the other.
|
|
9
|
+
3. imap.rb is unregistered, so this is its first version. bitget, eodhd, measurand and webdav each had their second name join at whatever the first stood at rather than backfilling, and each did it as a patch of its own carrying nothing else.
|
|
10
|
+
4. gem-publish builds and pushes each in turn, gem build and gem push taking one file apiece and gem build *.gemspec building only whichever the shell lists first.
|
|
11
|
+
5. ~ Gemfile: gemspec name: 'imap', bundler refusing to choose between two gemspecs and stopping with "There are multiple gemspecs at ...". Every pair here which uses gemspec names one: webdav, measurand and eodhd each name the elder of their two, and namo, which carried both from its first release, names the bare one. imap is the elder here.
|
|
12
|
+
6. ~ Imap::VERSION: /0.4.8/0.4.9/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
0.4.8: Rewrite the CHANGELOG in the standard form.
|
|
16
|
+
|
|
17
|
+
1. ~ CHANGELOG: ## before each date rather than ## [version] - date, the version and its summary beneath, a blank line between that and the first item, and the items numbered rather than dashed. No item is reworded, dropped or reordered.
|
|
18
|
+
2. ~ CHANGELOG: the dates in basic form, 20260910 for 2026-09-10, as every other gem here writes them.
|
|
19
|
+
3. ~ CHANGELOG: + the title, # imap/CHANGELOG, as coinmarketcap.rb and release-audit have it.
|
|
20
|
+
4. ~ CHANGELOG: the five releases of 20260917 under one date heading, which is how the form carries more than one in a day.
|
|
21
|
+
5. 9 of the 12 gems here were already in this form; imap and gate.rb were not, which leaves gate.rb.
|
|
22
|
+
6. ~ Imap::VERSION: /0.4.7/0.4.8/
|
|
23
|
+
|
|
24
|
+
0.4.7: ~ Imap::Search: the chaining interface, which the header has documented and which has never run
|
|
25
|
+
|
|
26
|
+
1. ~ 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.
|
|
27
|
+
2. ~ Imap::Search: the generated methods return self, having returned the criteria hash, so that they chain.
|
|
28
|
+
3. + 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.
|
|
29
|
+
4. ~ Imap::Search#general_to_imap_search_key: ['NOT', key, value] where the value is Negated.
|
|
30
|
+
5. ~ Imap::Search: the Usage header takes an imap_client, without which all() has nothing to search.
|
|
31
|
+
6. + tests: the lower case names, that they chain, both negations, and that not applies once only.
|
|
32
|
+
7. ~ Imap::VERSION: /0.4.6/0.4.7/
|
|
33
|
+
|
|
34
|
+
0.4.6: ~ Imap::Message#body: BODY.PEEK[TEXT]; ~ Imap::Search#to_imap_search_keys: raise upon a key which is neither
|
|
35
|
+
|
|
36
|
+
1. ~ 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].
|
|
37
|
+
2. ~ 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.
|
|
38
|
+
3. ~ Imap::Message#to: [] where the envelope carries no to, which raised NoMethodError upon nil.
|
|
39
|
+
4. ~ 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.
|
|
40
|
+
5. + a test apiece: that #body peeks, that #to is empty where there is no to, and that an unknown key raises.
|
|
41
|
+
6. ~ Imap::VERSION: /0.4.5/0.4.6/
|
|
42
|
+
|
|
43
|
+
0.4.5: + Gemfile; ~ imap.gemspec: dependencies=, - spec.date, + metadata
|
|
44
|
+
|
|
45
|
+
1. + 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.
|
|
46
|
+
2. ~ imap.gemspec: + 'Gemfile' to spec.files, which every other gem here ships.
|
|
47
|
+
3. ~ imap.gemspec: dependencies= and development_dependencies= through the Gem::Specification extension, as switches, eodhd and moby have it and as bitget, coinmarketcap and this did not. The constraints survive: the splat takes a bare string or a [name, constraint] pair alike.
|
|
48
|
+
4. ~ imap.gemspec: - spec.date = '2025-12-20', nine months and two versions stale, and the last one left in the estate. RubyGems answers 1980-01-02 in its place, the reproducible-build epoch, which is what coinmarketcap has carried since dropping its own at 0.6.0.
|
|
49
|
+
5. ~ imap.gemspec: + metadata, the four uris switches and moby declare, so that the RubyGems page links to the issues, the changelog, the source and the README. master here where moby says main, each by the branch it is on.
|
|
50
|
+
6. ~ Imap::VERSION: /0.4.4/0.4.5/
|
|
51
|
+
|
|
52
|
+
0.4.4: + Rakefile, - test/test_all.rb
|
|
53
|
+
|
|
54
|
+
1. + Rakefile: Rake::TestTask over FileList['test/**/*_test.rb'], with libs for lib and test, and warning = true. The minimal form nine of the gems here use rather than the fuller one five use: the :spec alias and the :version task belong to the minority, and a :version task reading VERSION.rb would not have caught 0.4.1, where require 'imap' left Imap::VERSION undefined while VERSION.rb sat there defining it. rake has been a development dependency since 0.4.0 with nothing here to run.
|
|
55
|
+
2. - test/test_all.rb: a second entry point which the Rakefile's FileList makes redundant, and which had drifted from the files on disk in two ways: it never loaded test/Imap/VERSION_test.rb, added in 0.4.2, so it ran 42 tests where there are 44; and it required './Imap_test' for a file named imap_test.rb, which resolved only because the filesystem is case insensitive and would not have on Linux. A glob cannot drift. 14 of the 16 gems here with a Rakefile carry no such file; switches keeps spec/all.rb because its Rakefile targets it, and bitget carries the same redundancy this removes.
|
|
56
|
+
3. ~ test/Imap/Message_test.rb: must_match(/Mock body/) for must_match /Mock body/, the ambiguous / which warning = true reports and which is the only warning the suite raises.
|
|
57
|
+
4. ~ imap.gemspec: + 'Rakefile' to spec.files, as eodhd, switches and moby have.
|
|
58
|
+
5. ~ Imap::VERSION: /0.4.3/0.4.4/
|
|
59
|
+
|
|
60
|
+
0.4.3: ~ test/test_helper.rb: + require 'minitest/mock', which minitest 6 no longer brings
|
|
61
|
+
|
|
62
|
+
1. ~ test/test_helper.rb: + `require 'minitest/mock'`. 35 of the 42 tests call stub and not one of them could: the suite answered "42 runs, 7 assertions, 0 failures, 35 errors". minitest 5's autorun required minitest/mock and shipped it, so the suite ran until minitest 6 dropped both the require and the file; the same thing coinmarketcap 0.6.1 found.
|
|
63
|
+
2. ~ imap.gemspec: + minitest-mock as a development dependency, which minitest ~> 5.0 carried and minitest 6 does not.
|
|
64
|
+
3. ~ Imap::VERSION: /0.4.2/0.4.3/
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## 20260910
|
|
68
|
+
|
|
69
|
+
0.4.2: ~ lib/imap.rb: + require_relative './Imap/VERSION'; + test/Imap/VERSION_test.rb
|
|
70
|
+
|
|
71
|
+
1. ~ lib/imap.rb: + `require_relative './Imap/VERSION'`, last among the requires. The file had been present and unreached, so `require 'imap'` left Imap::VERSION undefined and the gemspec was the only thing loading it.
|
|
72
|
+
2. + test/Imap/VERSION_test.rb: that Imap::VERSION is a string and that it is three numbers separated by dots. Modelled on moby's, less the example comparing against the changelog, which was unreachable here while the file was misspelt.
|
|
73
|
+
3. The test loads the library rather than VERSION.rb, which is the whole of its value: run against v0.4.1 it fails with "uninitialized constant Imap::VERSION". Nine libraries were in that state and not one of them carried this test.
|
|
74
|
+
4. ~ Imap::VERSION: /0.4.1/0.4.2/
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## 20260822
|
|
78
|
+
|
|
79
|
+
0.4.1: + LICENSE; ~ imap.gemspec: + CHANGELOG
|
|
80
|
+
|
|
81
|
+
1. + LICENSE: the MIT text, copyright 2025-2026 thoran. The gemspec had declared MIT while the repository carried no licence text at all.
|
|
82
|
+
2. ~ imap.gemspec: + 'LICENSE' and 'CHANGELOG' to spec.files, neither of which the gem had been shipping.
|
|
83
|
+
3. ~ Imap::VERSION: /0.4.0/0.4.1/
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## 20251220
|
|
87
|
+
|
|
88
|
+
0.4.0: Cleanup for publishing.
|
|
89
|
+
|
|
90
|
+
1. /ImapClient/Imap/
|
|
91
|
+
2. + imap.gemspec
|
|
92
|
+
3. + tests
|
|
93
|
+
4. + Imap::Message#to.
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 thoran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# imap
|
|
2
|
+
|
|
3
|
+
A Ruby wrapper around Net::IMAP with an elegant DSL for searching, reading, and managing email.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Clean, hash-based search interface
|
|
8
|
+
- Intuitive message accessors
|
|
9
|
+
- No Rails dependency - works anywhere
|
|
10
|
+
- Zero external dependencies beyond net-imap
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add this line to your application's Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'imap'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
And then execute:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
$ bundle install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install it yourself as:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
$ gem install imap
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
```ruby
|
|
34
|
+
require 'imap'
|
|
35
|
+
|
|
36
|
+
# Connect and authenticate
|
|
37
|
+
imap_client = Imap.setup(
|
|
38
|
+
server: 'imap.thoran.com',
|
|
39
|
+
username: 'code@thoran.com',
|
|
40
|
+
password: 'bigsecret',
|
|
41
|
+
mailbox: 'INBOX' # optional/default
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
messages = imap_client.search(
|
|
45
|
+
from: 'noreply@example.com',
|
|
46
|
+
subject: 'Payday Loans',
|
|
47
|
+
since: '18-DEC-2025',
|
|
48
|
+
seen: false
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Access message details
|
|
52
|
+
messages.each do |message|
|
|
53
|
+
puts message.subject
|
|
54
|
+
puts message.from
|
|
55
|
+
puts message.to
|
|
56
|
+
puts message.body
|
|
57
|
+
puts message.urls # Extracted URLs from body
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Mark messages as read
|
|
61
|
+
messages.first.mark_as_read
|
|
62
|
+
|
|
63
|
+
# Clean up
|
|
64
|
+
imap_client.bye
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
1. Fork it (https://github.com/thoran/imap/fork)
|
|
70
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
71
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
72
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
73
|
+
5. Create a new pull request
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
data/Rakefile
ADDED
data/imap.rb.gemspec
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# imap.rb.gemspec
|
|
2
|
+
|
|
3
|
+
require_relative './lib/Imap/VERSION'
|
|
4
|
+
|
|
5
|
+
class Gem::Specification
|
|
6
|
+
def dependencies=(gems)
|
|
7
|
+
gems.each{|gem| add_dependency(*gem)}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def development_dependencies=(gems)
|
|
11
|
+
gems.each{|gem| add_development_dependency(*gem)}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Gem::Specification.new do |spec|
|
|
16
|
+
spec.name = 'imap.rb'
|
|
17
|
+
|
|
18
|
+
spec.version = Imap::VERSION
|
|
19
|
+
|
|
20
|
+
spec.summary = "IMAP for Ruby made easy."
|
|
21
|
+
spec.description = "A Ruby wrapper around Net::IMAP with an elegant DSL for searching, reading, and managing email."
|
|
22
|
+
|
|
23
|
+
spec.author = 'thoran'
|
|
24
|
+
spec.email = 'code@thoran.com'
|
|
25
|
+
spec.homepage = 'https://github.com/thoran/imap'
|
|
26
|
+
spec.license = 'MIT'
|
|
27
|
+
|
|
28
|
+
spec.required_ruby_version = '>= 2.7'
|
|
29
|
+
|
|
30
|
+
spec.dependencies = [
|
|
31
|
+
['net-imap', '~> 0.4']
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
spec.development_dependencies = [
|
|
35
|
+
['minitest', '~> 5.0'],
|
|
36
|
+
'minitest-mock',
|
|
37
|
+
['rake', '~> 13.0']
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
spec.files = [
|
|
41
|
+
'imap.rb.gemspec',
|
|
42
|
+
Dir['lib/**/*.rb'],
|
|
43
|
+
Dir['test/**/*.rb'],
|
|
44
|
+
'CHANGELOG',
|
|
45
|
+
'Gemfile',
|
|
46
|
+
'Rakefile',
|
|
47
|
+
'LICENSE',
|
|
48
|
+
'README.md',
|
|
49
|
+
].flatten
|
|
50
|
+
spec.require_paths = ['lib']
|
|
51
|
+
|
|
52
|
+
spec.metadata = {
|
|
53
|
+
"bug_tracker_uri" => "https://github.com/thoran/imap/issues",
|
|
54
|
+
"changelog_uri" => "https://github.com/thoran/imap/blob/master/CHANGELOG",
|
|
55
|
+
"source_code_uri" => "https://github.com/thoran/imap",
|
|
56
|
+
"documentation_uri" => "https://github.com/thoran/imap/blob/master/README.md"
|
|
57
|
+
}
|
|
58
|
+
end
|
data/lib/Imap/Message.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Imap/Message.rb
|
|
2
|
+
# Imap::Message
|
|
3
|
+
|
|
4
|
+
# Usage:
|
|
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
|
+
|
|
14
|
+
require_relative './Search'
|
|
15
|
+
|
|
16
|
+
class Imap
|
|
17
|
+
class Message
|
|
18
|
+
class << self
|
|
19
|
+
|
|
20
|
+
def search(imap_client, **search_criteria)
|
|
21
|
+
message_ids = Imap::Search.new(imap_client, search_criteria).message_ids
|
|
22
|
+
message_ids.collect{|message_id| Imap::Message.new(message_id, imap_client)}
|
|
23
|
+
end
|
|
24
|
+
alias_method :find, :search
|
|
25
|
+
|
|
26
|
+
end # class << self
|
|
27
|
+
|
|
28
|
+
attr_accessor :imap_client
|
|
29
|
+
attr_accessor :message_id
|
|
30
|
+
|
|
31
|
+
def body
|
|
32
|
+
@body ||= fetch_data('BODY.PEEK[TEXT]').first.attr['BODY[TEXT]']
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def subject
|
|
36
|
+
@subject ||= fetch_data('BODY[HEADER.FIELDS (SUBJECT)]').first.attr['BODY[HEADER.FIELDS (SUBJECT)]'].sub(/^Subject: /, '').strip
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def from
|
|
40
|
+
@from ||= fetch_data('BODY[HEADER.FIELDS (FROM)]').first.attr['BODY[HEADER.FIELDS (FROM)]'].sub(/^From: /, '').strip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to
|
|
44
|
+
@to ||= (fetch_data('ENVELOPE').first.attr['ENVELOPE'].to || []).map{|addr| addr.mailbox + '@' + addr.host}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def urls
|
|
48
|
+
body.scan(/https?:\/\/[\S]+/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mark_as_seen
|
|
52
|
+
imap_client.imap.store(@message_id, '+FLAGS', [:Seen])
|
|
53
|
+
end
|
|
54
|
+
alias_method :mark_as_read, :mark_as_seen
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def initialize(message_id = nil, imap_client = nil)
|
|
59
|
+
@message_id = message_id
|
|
60
|
+
@imap_client = imap_client
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_data(*attrs)
|
|
64
|
+
@fetch_data = imap_client.imap.fetch(message_id, [*attrs])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/Imap/Search.rb
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Imap/Search.rb
|
|
2
|
+
# Imap::Search
|
|
3
|
+
|
|
4
|
+
# Usage:
|
|
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
|
+
|
|
8
|
+
# Notes:
|
|
9
|
+
# 1. List of search keys taken from RFC-3501 (INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1), http://tools.ietf.org/html/rfc3501.
|
|
10
|
+
|
|
11
|
+
class Imap
|
|
12
|
+
class Search
|
|
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
|
+
|
|
21
|
+
ALL_SEARCH_KEYS = %w{
|
|
22
|
+
ALL
|
|
23
|
+
ANSWERED
|
|
24
|
+
BCC
|
|
25
|
+
BEFORE
|
|
26
|
+
BODY
|
|
27
|
+
CC
|
|
28
|
+
DELETED
|
|
29
|
+
DRAFT
|
|
30
|
+
FLAGGED
|
|
31
|
+
FROM
|
|
32
|
+
HEADER
|
|
33
|
+
KEYWORD
|
|
34
|
+
LARGER
|
|
35
|
+
NEW
|
|
36
|
+
NOT
|
|
37
|
+
OLD
|
|
38
|
+
ON
|
|
39
|
+
OR
|
|
40
|
+
RECENT
|
|
41
|
+
SEEN
|
|
42
|
+
SENTBEFORE
|
|
43
|
+
SENTON
|
|
44
|
+
SENTSINCE
|
|
45
|
+
SINCE
|
|
46
|
+
SMALLER
|
|
47
|
+
SUBJECT
|
|
48
|
+
TEXT
|
|
49
|
+
TO
|
|
50
|
+
UID
|
|
51
|
+
UNANSWERED
|
|
52
|
+
UNDELETED
|
|
53
|
+
UNDRAFT
|
|
54
|
+
UNFLAGGED
|
|
55
|
+
UNKEYWORD
|
|
56
|
+
UNSEEN
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
BOOLEAN_SEARCH_KEYS = %w{
|
|
60
|
+
ANSWERED UNANSWERED
|
|
61
|
+
DELETED UNDELETED
|
|
62
|
+
DRAFT UNDRAFT
|
|
63
|
+
FLAGGED UNFLAGGED
|
|
64
|
+
NEW
|
|
65
|
+
OLD
|
|
66
|
+
RECENT
|
|
67
|
+
SEEN UNSEEN
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
GENERAL_SEARCH_KEYS = %w{
|
|
71
|
+
BCC
|
|
72
|
+
BEFORE
|
|
73
|
+
BODY
|
|
74
|
+
CC
|
|
75
|
+
FROM
|
|
76
|
+
HEADER
|
|
77
|
+
KEYWORD UNKEYWORD
|
|
78
|
+
LARGER
|
|
79
|
+
ON
|
|
80
|
+
SENTBEFORE
|
|
81
|
+
SENTON
|
|
82
|
+
SENTSINCE
|
|
83
|
+
SINCE
|
|
84
|
+
SMALLER
|
|
85
|
+
SUBJECT
|
|
86
|
+
TEXT
|
|
87
|
+
TO
|
|
88
|
+
UID
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
BOOLEAN_SEARCH_KEYS.each do |boolean_search_key|
|
|
92
|
+
define_method boolean_search_key.downcase do |value = true|
|
|
93
|
+
criteria.merge!(boolean_search_key.to_sym => negating? ? !value : value)
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
GENERAL_SEARCH_KEYS.each do |general_search_key|
|
|
99
|
+
define_method general_search_key.downcase do |value|
|
|
100
|
+
criteria.merge!(general_search_key.to_sym => negating? ? Negated.new(value) : value)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Applies to what follows immediately, and to nothing after that.
|
|
106
|
+
def not
|
|
107
|
+
@negate = true
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
attr_accessor :criteria
|
|
112
|
+
attr_accessor :imap_client
|
|
113
|
+
|
|
114
|
+
def general_operator?(search_key)
|
|
115
|
+
GENERAL_SEARCH_KEYS.include?(search_key)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def boolean_operator?(search_key)
|
|
119
|
+
BOOLEAN_SEARCH_KEYS.include?(search_key)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def to_imap_search_keys
|
|
123
|
+
criteria.inject([]) do |m, kv|
|
|
124
|
+
key, value = kv.first.to_s.upcase, kv.last
|
|
125
|
+
case
|
|
126
|
+
when general_operator?(key)
|
|
127
|
+
m << general_to_imap_search_key(key, value)
|
|
128
|
+
when boolean_operator?(key)
|
|
129
|
+
m << boolean_to_imap_search_key(key, value)
|
|
130
|
+
else
|
|
131
|
+
raise UnknownSearchKey, "no such search key: #{key}"
|
|
132
|
+
end
|
|
133
|
+
end.flatten
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def general_to_imap_search_key(key, value)
|
|
137
|
+
value.is_a?(Negated) ? ['NOT', key, value.value] : [key, value]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def boolean_to_imap_search_key(key, value)
|
|
141
|
+
if value
|
|
142
|
+
[key]
|
|
143
|
+
else
|
|
144
|
+
['NOT', key]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def negating?
|
|
149
|
+
negate = @negate
|
|
150
|
+
@negate = false
|
|
151
|
+
negate
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def message_ids
|
|
155
|
+
imap_client.imap.search(to_imap_search_keys)
|
|
156
|
+
end
|
|
157
|
+
alias_method :all, :message_ids
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def initialize(imap_client = nil, criteria = nil)
|
|
162
|
+
@criteria = criteria || {}
|
|
163
|
+
@imap_client = imap_client
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/Imap/VERSION.rb
ADDED
data/lib/imap.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# imap.rb
|
|
2
|
+
# Imap
|
|
3
|
+
|
|
4
|
+
require 'net/imap'
|
|
5
|
+
require_relative './Imap/Message'
|
|
6
|
+
require_relative './Imap/VERSION'
|
|
7
|
+
|
|
8
|
+
class Imap
|
|
9
|
+
class << self
|
|
10
|
+
|
|
11
|
+
def setup(config)
|
|
12
|
+
raise ArgumentError, "Server must be specified" unless config[:server]
|
|
13
|
+
imap_client = Imap.new(**config)
|
|
14
|
+
if config[:username] && config[:password]
|
|
15
|
+
imap_client.login(username: config[:username], password: config[:password])
|
|
16
|
+
end
|
|
17
|
+
if config[:mailbox]
|
|
18
|
+
imap_client.mailbox = config[:mailbox]
|
|
19
|
+
end
|
|
20
|
+
imap_client
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end # class << self
|
|
24
|
+
|
|
25
|
+
attr_accessor :server
|
|
26
|
+
attr_accessor :ssl
|
|
27
|
+
attr_accessor :username
|
|
28
|
+
attr_accessor :password
|
|
29
|
+
attr_reader :mailbox
|
|
30
|
+
|
|
31
|
+
def login(username: nil, password: nil)
|
|
32
|
+
username ||= @username
|
|
33
|
+
password ||= @password
|
|
34
|
+
begin
|
|
35
|
+
imap.login(username, password)
|
|
36
|
+
true
|
|
37
|
+
rescue Net::IMAP::NoResponseError, Net::IMAP::BadResponseError
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mailbox=(mailbox)
|
|
43
|
+
@mailbox = mailbox
|
|
44
|
+
imap.select(mailbox)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def search(criteria = {})
|
|
48
|
+
Imap::Message.search(self, **criteria)
|
|
49
|
+
end
|
|
50
|
+
alias_method :messages, :search
|
|
51
|
+
alias_method :find, :search
|
|
52
|
+
|
|
53
|
+
def bye
|
|
54
|
+
logout
|
|
55
|
+
disconnect
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def logout
|
|
59
|
+
imap.logout
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def disconnect
|
|
63
|
+
imap.disconnect
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def imap
|
|
67
|
+
@imap ||= Net::IMAP.new(server, ssl: @ssl)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ssl?
|
|
71
|
+
@ssl
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def initialize(server:, ssl: true, username: nil, password: nil, mailbox: 'INBOX')
|
|
77
|
+
@server = server
|
|
78
|
+
@ssl = ssl
|
|
79
|
+
@username = username
|
|
80
|
+
@password = password
|
|
81
|
+
@mailbox = mailbox
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# test/Imap/Message_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
|
|
5
|
+
describe Imap::Message do
|
|
6
|
+
let(:client) do
|
|
7
|
+
mock_imap = MockIMAP.new('imap.example.com', ssl: true)
|
|
8
|
+
Net::IMAP.stub(:new, mock_imap) do
|
|
9
|
+
Imap.setup(
|
|
10
|
+
server: 'imap.example.com',
|
|
11
|
+
username: 'user@example.com',
|
|
12
|
+
password: 'secret'
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:imap_message) do
|
|
18
|
+
Imap::Message.new(1, client)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '.search' do
|
|
22
|
+
it 'returns an array of messages' do
|
|
23
|
+
messages = Imap::Message.search(client, from: 'test@example.com')
|
|
24
|
+
_(messages).must_be_instance_of Array
|
|
25
|
+
_(messages.first).must_be_instance_of Imap::Message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'has find alias' do
|
|
29
|
+
_((Imap::Message.method(:find) == Imap::Message.method(:search))).must_equal true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '#body' do
|
|
34
|
+
it 'returns the message body' do
|
|
35
|
+
_(imap_message.body).must_match(/Mock body/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'caches the result' do
|
|
39
|
+
first_call = imap_message.body
|
|
40
|
+
second_call = imap_message.body
|
|
41
|
+
_(first_call.object_id).must_equal second_call.object_id
|
|
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
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#subject' do
|
|
51
|
+
it 'returns the subject without prefix' do
|
|
52
|
+
_(imap_message.subject).must_match(/Mock Subject/)
|
|
53
|
+
_(imap_message.subject).wont_match(/^Subject:/)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'strips whitespace' do
|
|
57
|
+
_(imap_message.subject).must_equal(imap_message.subject.strip)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe '#from' do
|
|
62
|
+
it 'returns the from address' do
|
|
63
|
+
_(imap_message.from).must_match(/sender@example.com/)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'removes From: prefix' do
|
|
67
|
+
_(imap_message.from).wont_match(/^From:/)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#to' do
|
|
72
|
+
it 'returns an array of email addresses' do
|
|
73
|
+
addresses = imap_message.to
|
|
74
|
+
_(addresses).must_be_instance_of(Array)
|
|
75
|
+
_(addresses.first).must_match(/@/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'formats addresses correctly' do
|
|
79
|
+
addresses = imap_message.to
|
|
80
|
+
_(addresses.first).must_equal('user@example.com')
|
|
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
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe '#urls' do
|
|
89
|
+
it 'extracts http URLs from body' do
|
|
90
|
+
imap_message.stub(:body, 'Check out http://example.com and https://test.com') do
|
|
91
|
+
urls = imap_message.urls
|
|
92
|
+
_(urls).must_include('http://example.com')
|
|
93
|
+
_(urls).must_include('https://test.com')
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns empty array when no URLs' do
|
|
98
|
+
imap_message.stub(:body, 'No URLs here') do
|
|
99
|
+
_(urls = imap_message.urls).must_be_empty
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '#mark_as_seen' do
|
|
105
|
+
it 'marks the message as seen' do
|
|
106
|
+
_(imap_message.mark_as_seen).must_be_nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'has mark_as_read alias' do
|
|
110
|
+
_((imap_message.method(:mark_as_read) == imap_message.method(:mark_as_seen))).must_equal true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#initialize' do
|
|
115
|
+
it 'accepts message_id and imap_client' do
|
|
116
|
+
test_message = Imap::Message.new(42, client)
|
|
117
|
+
_(test_message.message_id).must_equal(42)
|
|
118
|
+
_(test_message.imap_client).must_equal(client)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# test/Imap/Search_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
|
|
5
|
+
describe Imap::Search do
|
|
6
|
+
let(:client) do
|
|
7
|
+
mock_imap = MockIMAP.new('imap.example.com', ssl: true)
|
|
8
|
+
Net::IMAP.stub(:new, mock_imap) do
|
|
9
|
+
Imap.setup(
|
|
10
|
+
server: 'imap.example.com',
|
|
11
|
+
username: 'user@example.com',
|
|
12
|
+
password: 'secret'
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:search) do
|
|
18
|
+
Imap::Search.new(client, {})
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#to_imap_search_keys' do
|
|
22
|
+
it 'converts general search keys' do
|
|
23
|
+
test_search = Imap::Search.new(client, from: 'test@example.com')
|
|
24
|
+
keys = test_search.to_imap_search_keys
|
|
25
|
+
_(keys).must_include 'FROM'
|
|
26
|
+
_(keys).must_include 'test@example.com'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'converts boolean search keys' do
|
|
30
|
+
test_search = Imap::Search.new(client, seen: false)
|
|
31
|
+
keys = test_search.to_imap_search_keys
|
|
32
|
+
_(keys).must_include 'NOT'
|
|
33
|
+
_(keys).must_include 'SEEN'
|
|
34
|
+
end
|
|
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
|
+
|
|
40
|
+
it 'handles multiple criteria' do
|
|
41
|
+
test_search = Imap::Search.new(client, from: 'test@example.com', seen: true)
|
|
42
|
+
keys = test_search.to_imap_search_keys
|
|
43
|
+
_(keys.length).must_be :>, 2
|
|
44
|
+
end
|
|
45
|
+
end
|
|
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
|
+
|
|
71
|
+
describe '#message_ids' do
|
|
72
|
+
it 'returns an array of message IDs' do
|
|
73
|
+
ids = search.message_ids
|
|
74
|
+
_(ids).must_be_instance_of Array
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'has all alias' do
|
|
78
|
+
_((search.method(:all) == search.method(:message_ids))).must_equal true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#boolean_operator?' do
|
|
83
|
+
it 'returns true for boolean operators' do
|
|
84
|
+
_(search.boolean_operator?('SEEN')).must_equal true
|
|
85
|
+
_(search.boolean_operator?('ANSWERED')).must_equal true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns false for general operators' do
|
|
89
|
+
_(search.boolean_operator?('FROM')).must_equal false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#general_operator?' do
|
|
94
|
+
it 'returns true for general operators' do
|
|
95
|
+
_(search.general_operator?('FROM')).must_equal true
|
|
96
|
+
_(search.general_operator?('SUBJECT')).must_equal true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'returns false for boolean operators' do
|
|
100
|
+
_(search.general_operator?('SEEN')).must_equal false
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '#initialize' do
|
|
105
|
+
it 'accepts imap_client parameter' do
|
|
106
|
+
test_search = Imap::Search.new(client)
|
|
107
|
+
_(test_search.imap_client).must_equal client
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'accepts criteria parameter' do
|
|
111
|
+
test_search = Imap::Search.new(client, from: 'test@example.com')
|
|
112
|
+
_(test_search.criteria).must_equal({ from: 'test@example.com' })
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'defaults criteria to empty hash' do
|
|
116
|
+
test_search = Imap::Search.new(client)
|
|
117
|
+
_(test_search.criteria).must_equal({})
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# test/Imap/VERSION_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
|
|
5
|
+
describe Imap do
|
|
6
|
+
describe "VERSION" do
|
|
7
|
+
it "is a string" do
|
|
8
|
+
_(Imap::VERSION).must_be_instance_of String
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "is three numbers separated by dots" do
|
|
12
|
+
_(Imap::VERSION).must_match(/\A\d+\.\d+\.\d+\z/)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/test/Imap_test.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# test/imap_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative './test_helper'
|
|
4
|
+
|
|
5
|
+
describe Imap do
|
|
6
|
+
let(:client) do
|
|
7
|
+
Net::IMAP.stub(:new, MockIMAP.new('imap.example.com', ssl: true)) do
|
|
8
|
+
Imap.setup(
|
|
9
|
+
server: 'imap.example.com',
|
|
10
|
+
username: 'user@example.com',
|
|
11
|
+
password: 'secret',
|
|
12
|
+
mailbox: 'INBOX'
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe ".setup" do
|
|
18
|
+
it 'creates a new client instance' do
|
|
19
|
+
_(client).must_be_instance_of(Imap)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'requires a server parameter' do
|
|
23
|
+
_(proc{Imap.setup({})}).must_raise(ArgumentError)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'sets the server' do
|
|
27
|
+
_(client.server).must_equal 'imap.example.com'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'defaults ssl to true' do
|
|
31
|
+
_(client.ssl?).must_equal true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'defaults mailbox to INBOX' do
|
|
35
|
+
_(client.mailbox).must_equal 'INBOX'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "#login" do
|
|
40
|
+
it 'returns true on successful login' do
|
|
41
|
+
Net::IMAP.stub(:new, MockIMAP.new('imap.example.com', ssl: true)) do
|
|
42
|
+
test_client = Imap.new(server: 'imap.example.com')
|
|
43
|
+
result = test_client.login(username: 'user', password: 'pass')
|
|
44
|
+
_(result).must_equal true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns false on failed login' do
|
|
49
|
+
mock = MockIMAP.new('imap.example.com', ssl: true)
|
|
50
|
+
def mock.login(username, password)
|
|
51
|
+
mock_response = OpenStruct.new(data: OpenStruct.new(text: 'Authentication failed'))
|
|
52
|
+
raise Net::IMAP::BadResponseError, mock_response
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Net::IMAP.stub(:new, mock) do
|
|
56
|
+
test_client = Imap.new(server: 'imap.example.com')
|
|
57
|
+
result = test_client.login(username: 'user', password: 'wrong')
|
|
58
|
+
_(result).must_equal false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "#mailbox=" do
|
|
64
|
+
it 'sets the mailbox' do
|
|
65
|
+
Net::IMAP.stub(:new, MockIMAP.new('imap.example.com', ssl: true)) do
|
|
66
|
+
test_client = Imap.new(server: 'imap.example.com')
|
|
67
|
+
test_client.mailbox = 'Sent'
|
|
68
|
+
_(test_client.mailbox).must_equal 'Sent'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#search" do
|
|
74
|
+
it 'returns an array of Message objects' do
|
|
75
|
+
Net::IMAP.stub(:new, MockIMAP.new('imap.example.com', ssl: true)) do
|
|
76
|
+
test_client = Imap.setup(
|
|
77
|
+
server: 'imap.example.com',
|
|
78
|
+
username: 'user',
|
|
79
|
+
password: 'pass'
|
|
80
|
+
)
|
|
81
|
+
messages = test_client.search(from: 'test@example.com')
|
|
82
|
+
_(messages).must_be_instance_of Array
|
|
83
|
+
_(messages.first).must_be_instance_of Imap::Message
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'has messages and find aliases' do
|
|
88
|
+
_((client.method(:messages) == client.method(:search))).must_equal true
|
|
89
|
+
_((client.method(:find) == client.method(:search))).must_equal true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe "#initialize" do
|
|
94
|
+
it 'accepts server parameter' do
|
|
95
|
+
test_client = Imap.new(server: 'mail.example.com')
|
|
96
|
+
_(test_client.server).must_equal 'mail.example.com'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'accepts ssl parameter' do
|
|
100
|
+
test_client = Imap.new(server: 'mail.example.com', ssl: false)
|
|
101
|
+
_(test_client.ssl?).must_equal false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'accepts username parameter' do
|
|
105
|
+
test_client = Imap.new(server: 'mail.example.com', username: 'test@example.com')
|
|
106
|
+
_(test_client.username).must_equal 'test@example.com'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'accepts password parameter' do
|
|
110
|
+
test_client = Imap.new(server: 'mail.example.com', password: 'secret')
|
|
111
|
+
_(test_client.password).must_equal 'secret'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'accepts mailbox parameter' do
|
|
115
|
+
test_client = Imap.new(server: 'mail.example.com', mailbox: 'Sent')
|
|
116
|
+
_(test_client.mailbox).must_equal 'Sent'
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# test/test_helper.rb
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest/mock'
|
|
5
|
+
require 'minitest/spec'
|
|
6
|
+
require 'ostruct'
|
|
7
|
+
require_relative '../lib/imap'
|
|
8
|
+
|
|
9
|
+
class MockIMAP
|
|
10
|
+
attr_reader :login_called
|
|
11
|
+
attr_reader :logout_called
|
|
12
|
+
attr_reader :search_called
|
|
13
|
+
attr_reader :search_criteria
|
|
14
|
+
attr_reader :select_called
|
|
15
|
+
attr_reader :selected_mailbox
|
|
16
|
+
|
|
17
|
+
def login(username, password)
|
|
18
|
+
@login_called = true
|
|
19
|
+
@username = username
|
|
20
|
+
@password = password
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def select(mailbox)
|
|
24
|
+
@select_called = true
|
|
25
|
+
@selected_mailbox = mailbox
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def search(criteria)
|
|
29
|
+
@search_called = true
|
|
30
|
+
@search_criteria = criteria
|
|
31
|
+
[1, 2, 3]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :fetched_attrs
|
|
35
|
+
|
|
36
|
+
def fetch(message_id, attrs)
|
|
37
|
+
@fetched_attrs = attrs
|
|
38
|
+
[OpenStruct.new(attr: mock_fetch_attrs(message_id, attrs))]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def store(message_id, flags, values); end
|
|
42
|
+
|
|
43
|
+
def logout
|
|
44
|
+
@logout_called = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def disconnect; end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def initialize(server, ssl:)
|
|
52
|
+
@server = server
|
|
53
|
+
@ssl = ssl
|
|
54
|
+
@login_called = false
|
|
55
|
+
@select_called = false
|
|
56
|
+
@search_called = false
|
|
57
|
+
@logout_called = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mock_fetch_attrs(message_id, attrs)
|
|
61
|
+
result = {}
|
|
62
|
+
attrs.each do |attr|
|
|
63
|
+
case attr
|
|
64
|
+
when 'BODY[TEXT]', 'BODY.PEEK[TEXT]'
|
|
65
|
+
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
|
+
when 'ENVELOPE'
|
|
71
|
+
result['ENVELOPE'] = OpenStruct.new(
|
|
72
|
+
to: message_id == 99 ? nil : [OpenStruct.new(mailbox: 'user', host: 'example.com')]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: imap.rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.9
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- thoran
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: net-imap
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.4'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.4'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest-mock
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
description: A Ruby wrapper around Net::IMAP with an elegant DSL for searching, reading,
|
|
69
|
+
and managing email.
|
|
70
|
+
email: code@thoran.com
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- CHANGELOG
|
|
76
|
+
- Gemfile
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- Rakefile
|
|
80
|
+
- imap.rb.gemspec
|
|
81
|
+
- lib/Imap/Message.rb
|
|
82
|
+
- lib/Imap/Search.rb
|
|
83
|
+
- lib/Imap/VERSION.rb
|
|
84
|
+
- lib/imap.rb
|
|
85
|
+
- test/Imap/Message_test.rb
|
|
86
|
+
- test/Imap/Search_test.rb
|
|
87
|
+
- test/Imap/VERSION_test.rb
|
|
88
|
+
- test/Imap_test.rb
|
|
89
|
+
- test/test_helper.rb
|
|
90
|
+
homepage: https://github.com/thoran/imap
|
|
91
|
+
licenses:
|
|
92
|
+
- MIT
|
|
93
|
+
metadata:
|
|
94
|
+
bug_tracker_uri: https://github.com/thoran/imap/issues
|
|
95
|
+
changelog_uri: https://github.com/thoran/imap/blob/master/CHANGELOG
|
|
96
|
+
source_code_uri: https://github.com/thoran/imap
|
|
97
|
+
documentation_uri: https://github.com/thoran/imap/blob/master/README.md
|
|
98
|
+
rdoc_options: []
|
|
99
|
+
require_paths:
|
|
100
|
+
- lib
|
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '2.7'
|
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
requirements: []
|
|
112
|
+
rubygems_version: 4.0.20
|
|
113
|
+
specification_version: 4
|
|
114
|
+
summary: IMAP for Ruby made easy.
|
|
115
|
+
test_files: []
|