imap 0.4.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 +7 -0
- data/README.md +77 -0
- data/imap.gemspec +33 -0
- data/lib/Imap/Message.rb +67 -0
- data/lib/Imap/Search.rb +143 -0
- data/lib/Imap/VERSION.rb +3 -0
- data/lib/imap.rb +82 -0
- data/test/Imap/Message_test.rb +112 -0
- data/test/Imap/Search_test.rb +92 -0
- data/test/imap_test.rb +119 -0
- data/test/test_all.rb +6 -0
- data/test/test_helper.rb +74 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 587a50234d8535c27ceeace7941c30f74147ef68bb65e835eb84b04878bc91c7
|
|
4
|
+
data.tar.gz: 2778297913ec8a25dcb39e014cff11c538b8eee0bb706846d13045f744995096
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fb5a7c2332893c2a2fd9c0728480a6c098302d14a389986c1003cc5b547ec85a9dbff6ab4f7927bfdc5039b45d54719da17a1b1b4e0e88f35a03d741cb096313
|
|
7
|
+
data.tar.gz: 8ac81274dd278a5fd71fa3434837df50c39a73db7e311e9ddfdc15e51e7320992383987a129f321cbda3a6ad8c2a8da55ab02c272a713e2ed8a9ff72f7af0768
|
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/imap.gemspec
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# imap.gemspec
|
|
2
|
+
|
|
3
|
+
require_relative './lib/Imap/VERSION'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'imap'
|
|
7
|
+
|
|
8
|
+
spec.version = Imap::VERSION
|
|
9
|
+
spec.date = '2025-12-20'
|
|
10
|
+
|
|
11
|
+
spec.summary = "IMAP for Ruby made easy."
|
|
12
|
+
spec.description = "A Ruby wrapper around Net::IMAP with an elegant DSL for searching, reading, and managing email."
|
|
13
|
+
|
|
14
|
+
spec.author = 'thoran'
|
|
15
|
+
spec.email = 'code@thoran.com'
|
|
16
|
+
spec.homepage = 'https://github.com/thoran/imap'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
|
|
19
|
+
spec.required_ruby_version = '>= 2.7'
|
|
20
|
+
|
|
21
|
+
spec.add_dependency 'net-imap', "~> 0.4"
|
|
22
|
+
|
|
23
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
|
24
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
25
|
+
|
|
26
|
+
spec.files = [
|
|
27
|
+
'imap.gemspec',
|
|
28
|
+
Dir['lib/**/*.rb'],
|
|
29
|
+
'README.md',
|
|
30
|
+
Dir['test/**/*.rb']
|
|
31
|
+
].flatten
|
|
32
|
+
spec.require_paths = ['lib']
|
|
33
|
+
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[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,143 @@
|
|
|
1
|
+
# Imap/Search.rb
|
|
2
|
+
# Imap::Search
|
|
3
|
+
|
|
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})
|
|
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
|
+
ALL_SEARCH_KEYS = %w{
|
|
15
|
+
ALL
|
|
16
|
+
ANSWERED
|
|
17
|
+
BCC
|
|
18
|
+
BEFORE
|
|
19
|
+
BODY
|
|
20
|
+
CC
|
|
21
|
+
DELETED
|
|
22
|
+
DRAFT
|
|
23
|
+
FLAGGED
|
|
24
|
+
FROM
|
|
25
|
+
HEADER
|
|
26
|
+
KEYWORD
|
|
27
|
+
LARGER
|
|
28
|
+
NEW
|
|
29
|
+
NOT
|
|
30
|
+
OLD
|
|
31
|
+
ON
|
|
32
|
+
OR
|
|
33
|
+
RECENT
|
|
34
|
+
SEEN
|
|
35
|
+
SENTBEFORE
|
|
36
|
+
SENTON
|
|
37
|
+
SENTSINCE
|
|
38
|
+
SINCE
|
|
39
|
+
SMALLER
|
|
40
|
+
SUBJECT
|
|
41
|
+
TEXT
|
|
42
|
+
TO
|
|
43
|
+
UID
|
|
44
|
+
UNANSWERED
|
|
45
|
+
UNDELETED
|
|
46
|
+
UNDRAFT
|
|
47
|
+
UNFLAGGED
|
|
48
|
+
UNKEYWORD
|
|
49
|
+
UNSEEN
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
BOOLEAN_SEARCH_KEYS = %w{
|
|
53
|
+
ANSWERED UNANSWERED
|
|
54
|
+
DELETED UNDELETED
|
|
55
|
+
DRAFT UNDRAFT
|
|
56
|
+
FLAGGED UNFLAGGED
|
|
57
|
+
NEW
|
|
58
|
+
OLD
|
|
59
|
+
RECENT
|
|
60
|
+
SEEN UNSEEN
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
GENERAL_SEARCH_KEYS = %w{
|
|
64
|
+
BCC
|
|
65
|
+
BEFORE
|
|
66
|
+
BODY
|
|
67
|
+
CC
|
|
68
|
+
FROM
|
|
69
|
+
HEADER
|
|
70
|
+
KEYWORD UNKEYWORD
|
|
71
|
+
LARGER
|
|
72
|
+
ON
|
|
73
|
+
SENTBEFORE
|
|
74
|
+
SENTON
|
|
75
|
+
SENTSINCE
|
|
76
|
+
SINCE
|
|
77
|
+
SMALLER
|
|
78
|
+
SUBJECT
|
|
79
|
+
TEXT
|
|
80
|
+
TO
|
|
81
|
+
UID
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
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)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
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)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
attr_accessor :criteria
|
|
97
|
+
attr_accessor :imap_client
|
|
98
|
+
|
|
99
|
+
def general_operator?(search_key)
|
|
100
|
+
GENERAL_SEARCH_KEYS.include?(search_key)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def boolean_operator?(search_key)
|
|
104
|
+
BOOLEAN_SEARCH_KEYS.include?(search_key)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_imap_search_keys
|
|
108
|
+
criteria.inject([]) do |m, kv|
|
|
109
|
+
key, value = kv.first.to_s.upcase, kv.last
|
|
110
|
+
case
|
|
111
|
+
when general_operator?(key)
|
|
112
|
+
m << general_to_imap_search_key(key, value)
|
|
113
|
+
when boolean_operator?(key)
|
|
114
|
+
m << boolean_to_imap_search_key(key, value)
|
|
115
|
+
end
|
|
116
|
+
end.flatten
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def general_to_imap_search_key(key, value)
|
|
120
|
+
[key, value]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def boolean_to_imap_search_key(key, value)
|
|
124
|
+
if value
|
|
125
|
+
[key]
|
|
126
|
+
else
|
|
127
|
+
['NOT', key]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def message_ids
|
|
132
|
+
imap_client.imap.search(to_imap_search_keys)
|
|
133
|
+
end
|
|
134
|
+
alias_method :all, :message_ids
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def initialize(imap_client = nil, criteria = nil)
|
|
139
|
+
@criteria = criteria || {}
|
|
140
|
+
@imap_client = imap_client
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/Imap/VERSION.rb
ADDED
data/lib/imap.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# imap.rb
|
|
2
|
+
# Imap
|
|
3
|
+
|
|
4
|
+
require 'net/imap'
|
|
5
|
+
require_relative './Imap/Message'
|
|
6
|
+
|
|
7
|
+
class Imap
|
|
8
|
+
class << self
|
|
9
|
+
|
|
10
|
+
def setup(config)
|
|
11
|
+
raise ArgumentError, "Server must be specified" unless config[:server]
|
|
12
|
+
imap_client = Imap.new(**config)
|
|
13
|
+
if config[:username] && config[:password]
|
|
14
|
+
imap_client.login(username: config[:username], password: config[:password])
|
|
15
|
+
end
|
|
16
|
+
if config[:mailbox]
|
|
17
|
+
imap_client.mailbox = config[:mailbox]
|
|
18
|
+
end
|
|
19
|
+
imap_client
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end # class << self
|
|
23
|
+
|
|
24
|
+
attr_accessor :server
|
|
25
|
+
attr_accessor :ssl
|
|
26
|
+
attr_accessor :username
|
|
27
|
+
attr_accessor :password
|
|
28
|
+
attr_reader :mailbox
|
|
29
|
+
|
|
30
|
+
def login(username: nil, password: nil)
|
|
31
|
+
username ||= @username
|
|
32
|
+
password ||= @password
|
|
33
|
+
begin
|
|
34
|
+
imap.login(username, password)
|
|
35
|
+
true
|
|
36
|
+
rescue Net::IMAP::NoResponseError, Net::IMAP::BadResponseError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mailbox=(mailbox)
|
|
42
|
+
@mailbox = mailbox
|
|
43
|
+
imap.select(mailbox)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def search(criteria = {})
|
|
47
|
+
Imap::Message.search(self, **criteria)
|
|
48
|
+
end
|
|
49
|
+
alias_method :messages, :search
|
|
50
|
+
alias_method :find, :search
|
|
51
|
+
|
|
52
|
+
def bye
|
|
53
|
+
logout
|
|
54
|
+
disconnect
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def logout
|
|
58
|
+
imap.logout
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def disconnect
|
|
62
|
+
imap.disconnect
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def imap
|
|
66
|
+
@imap ||= Net::IMAP.new(server, ssl: @ssl)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ssl?
|
|
70
|
+
@ssl
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def initialize(server:, ssl: true, username: nil, password: nil, mailbox: 'INBOX')
|
|
76
|
+
@server = server
|
|
77
|
+
@ssl = ssl
|
|
78
|
+
@username = username
|
|
79
|
+
@password = password
|
|
80
|
+
@mailbox = mailbox
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#subject' do
|
|
46
|
+
it 'returns the subject without prefix' do
|
|
47
|
+
_(imap_message.subject).must_match(/Mock Subject/)
|
|
48
|
+
_(imap_message.subject).wont_match(/^Subject:/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'strips whitespace' do
|
|
52
|
+
_(imap_message.subject).must_equal(imap_message.subject.strip)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#from' do
|
|
57
|
+
it 'returns the from address' do
|
|
58
|
+
_(imap_message.from).must_match(/sender@example.com/)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'removes From: prefix' do
|
|
62
|
+
_(imap_message.from).wont_match(/^From:/)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#to' do
|
|
67
|
+
it 'returns an array of email addresses' do
|
|
68
|
+
addresses = imap_message.to
|
|
69
|
+
_(addresses).must_be_instance_of(Array)
|
|
70
|
+
_(addresses.first).must_match(/@/)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'formats addresses correctly' do
|
|
74
|
+
addresses = imap_message.to
|
|
75
|
+
_(addresses.first).must_equal('user@example.com')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#urls' do
|
|
80
|
+
it 'extracts http URLs from body' do
|
|
81
|
+
imap_message.stub(:body, 'Check out http://example.com and https://test.com') do
|
|
82
|
+
urls = imap_message.urls
|
|
83
|
+
_(urls).must_include('http://example.com')
|
|
84
|
+
_(urls).must_include('https://test.com')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns empty array when no URLs' do
|
|
89
|
+
imap_message.stub(:body, 'No URLs here') do
|
|
90
|
+
_(urls = imap_message.urls).must_be_empty
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '#mark_as_seen' do
|
|
96
|
+
it 'marks the message as seen' do
|
|
97
|
+
_(imap_message.mark_as_seen).must_be_nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'has mark_as_read alias' do
|
|
101
|
+
_((imap_message.method(:mark_as_read) == imap_message.method(:mark_as_seen))).must_equal true
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '#initialize' do
|
|
106
|
+
it 'accepts message_id and imap_client' do
|
|
107
|
+
test_message = Imap::Message.new(42, client)
|
|
108
|
+
_(test_message.message_id).must_equal(42)
|
|
109
|
+
_(test_message.imap_client).must_equal(client)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
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 'handles multiple criteria' do
|
|
37
|
+
test_search = Imap::Search.new(client, from: 'test@example.com', seen: true)
|
|
38
|
+
keys = test_search.to_imap_search_keys
|
|
39
|
+
_(keys.length).must_be :>, 2
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#message_ids' do
|
|
44
|
+
it 'returns an array of message IDs' do
|
|
45
|
+
ids = search.message_ids
|
|
46
|
+
_(ids).must_be_instance_of Array
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'has all alias' do
|
|
50
|
+
_((search.method(:all) == search.method(:message_ids))).must_equal true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#boolean_operator?' do
|
|
55
|
+
it 'returns true for boolean operators' do
|
|
56
|
+
_(search.boolean_operator?('SEEN')).must_equal true
|
|
57
|
+
_(search.boolean_operator?('ANSWERED')).must_equal true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns false for general operators' do
|
|
61
|
+
_(search.boolean_operator?('FROM')).must_equal false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#general_operator?' do
|
|
66
|
+
it 'returns true for general operators' do
|
|
67
|
+
_(search.general_operator?('FROM')).must_equal true
|
|
68
|
+
_(search.general_operator?('SUBJECT')).must_equal true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns false for boolean operators' do
|
|
72
|
+
_(search.general_operator?('SEEN')).must_equal false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#initialize' do
|
|
77
|
+
it 'accepts imap_client parameter' do
|
|
78
|
+
test_search = Imap::Search.new(client)
|
|
79
|
+
_(test_search.imap_client).must_equal client
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'accepts criteria parameter' do
|
|
83
|
+
test_search = Imap::Search.new(client, from: 'test@example.com')
|
|
84
|
+
_(test_search.criteria).must_equal({ from: 'test@example.com' })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'defaults criteria to empty hash' do
|
|
88
|
+
test_search = Imap::Search.new(client)
|
|
89
|
+
_(test_search.criteria).must_equal({})
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
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_all.rb
ADDED
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# test/test_helper.rb
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest/spec'
|
|
5
|
+
require 'ostruct'
|
|
6
|
+
require_relative '../lib/imap'
|
|
7
|
+
|
|
8
|
+
class MockIMAP
|
|
9
|
+
attr_reader :login_called
|
|
10
|
+
attr_reader :logout_called
|
|
11
|
+
attr_reader :search_called
|
|
12
|
+
attr_reader :search_criteria
|
|
13
|
+
attr_reader :select_called
|
|
14
|
+
attr_reader :selected_mailbox
|
|
15
|
+
|
|
16
|
+
def login(username, password)
|
|
17
|
+
@login_called = true
|
|
18
|
+
@username = username
|
|
19
|
+
@password = password
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def select(mailbox)
|
|
23
|
+
@select_called = true
|
|
24
|
+
@selected_mailbox = mailbox
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def search(criteria)
|
|
28
|
+
@search_called = true
|
|
29
|
+
@search_criteria = criteria
|
|
30
|
+
[1, 2, 3]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(message_id, attrs)
|
|
34
|
+
[OpenStruct.new(attr: mock_fetch_attrs(message_id, attrs))]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def store(message_id, flags, values); end
|
|
38
|
+
|
|
39
|
+
def logout
|
|
40
|
+
@logout_called = true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def disconnect; end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def initialize(server, ssl:)
|
|
48
|
+
@server = server
|
|
49
|
+
@ssl = ssl
|
|
50
|
+
@login_called = false
|
|
51
|
+
@select_called = false
|
|
52
|
+
@search_called = false
|
|
53
|
+
@logout_called = false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def mock_fetch_attrs(message_id, attrs)
|
|
57
|
+
result = {}
|
|
58
|
+
attrs.each do |attr|
|
|
59
|
+
case attr
|
|
60
|
+
when 'BODY[TEXT]'
|
|
61
|
+
result['BODY[TEXT]'] = "Mock body for message #{message_id}"
|
|
62
|
+
when 'BODY[HEADER.FIELDS (SUBJECT)]'
|
|
63
|
+
result['BODY[HEADER.FIELDS (SUBJECT)]'] = "Subject: Mock Subject #{message_id}\r\n"
|
|
64
|
+
when 'BODY[HEADER.FIELDS (FROM)]'
|
|
65
|
+
result['BODY[HEADER.FIELDS (FROM)]'] = "From: sender@example.com\r\n"
|
|
66
|
+
when 'ENVELOPE'
|
|
67
|
+
result['ENVELOPE'] = OpenStruct.new(
|
|
68
|
+
to: [OpenStruct.new(mailbox: 'user', host: 'example.com')]
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: imap
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- thoran
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2025-12-20 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: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
description: A Ruby wrapper around Net::IMAP with an elegant DSL for searching, reading,
|
|
55
|
+
and managing email.
|
|
56
|
+
email: code@thoran.com
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- README.md
|
|
62
|
+
- imap.gemspec
|
|
63
|
+
- lib/Imap/Message.rb
|
|
64
|
+
- lib/Imap/Search.rb
|
|
65
|
+
- lib/Imap/VERSION.rb
|
|
66
|
+
- lib/imap.rb
|
|
67
|
+
- test/Imap/Message_test.rb
|
|
68
|
+
- test/Imap/Search_test.rb
|
|
69
|
+
- test/imap_test.rb
|
|
70
|
+
- test/test_all.rb
|
|
71
|
+
- test/test_helper.rb
|
|
72
|
+
homepage: https://github.com/thoran/imap
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata: {}
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '2.7'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 4.0.2
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: IMAP for Ruby made easy.
|
|
93
|
+
test_files: []
|