mail_daemon 0.1.0 → 1.0.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 +5 -5
- data/.rspec +3 -0
- data/.ruby-version +1 -1
- data/lib/mail_daemon/email_body_parser.rb +2 -2
- data/lib/mail_daemon/helpers.rb +2 -1
- data/lib/mail_daemon/imap/connection.rb +1 -1
- data/lib/mail_daemon/imap/watcher.rb +1 -0
- data/lib/mail_daemon/imap_watcher.rb +11 -8
- data/lib/mail_daemon/version.rb +1 -1
- data/lib/mail_daemon.rb +3 -1
- data/mail_daemon.gemspec +9 -2
- data/spec/examples.txt +147 -0
- data/spec/mail_daemon/email_body_parser_spec.rb +114 -0
- data/spec/mail_daemon/email_handler_spec.rb +309 -0
- data/spec/mail_daemon/encryption_spec.rb +161 -0
- data/spec/mail_daemon/handler_spec.rb +233 -0
- data/spec/mail_daemon/helpers_spec.rb +144 -0
- data/spec/mail_daemon/imap/connection_spec.rb +306 -0
- data/spec/mail_daemon/imap/statuses_spec.rb +47 -0
- data/spec/mail_daemon/imap/watcher_spec.rb +128 -0
- data/spec/mail_daemon/imap_watcher_spec.rb +205 -0
- data/spec/mail_daemon/version_spec.rb +15 -0
- data/spec/spec_helper.rb +36 -0
- metadata +142 -9
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe MailDaemon::Helpers do
|
|
4
|
+
let(:test_class) do
|
|
5
|
+
Class.new do
|
|
6
|
+
include MailDaemon::Helpers
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
let(:instance) { test_class.new }
|
|
11
|
+
|
|
12
|
+
describe '#setup_options' do
|
|
13
|
+
it 'converts string keys to symbols' do
|
|
14
|
+
options = { 'key1' => 'value1', 'key2' => 'value2' }
|
|
15
|
+
instance.setup_options(options)
|
|
16
|
+
|
|
17
|
+
expect(instance.instance_variable_get(:@options)).to eq(
|
|
18
|
+
key1: 'value1',
|
|
19
|
+
key2: 'value2'
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'preserves symbol keys' do
|
|
24
|
+
options = { key1: 'value1', key2: 'value2' }
|
|
25
|
+
instance.setup_options(options)
|
|
26
|
+
|
|
27
|
+
expect(instance.instance_variable_get(:@options)).to eq(
|
|
28
|
+
key1: 'value1',
|
|
29
|
+
key2: 'value2'
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'handles mixed string and symbol keys' do
|
|
34
|
+
options = { 'string_key' => 'value1', symbol_key: 'value2' }
|
|
35
|
+
instance.setup_options(options)
|
|
36
|
+
|
|
37
|
+
expect(instance.instance_variable_get(:@options)).to include(
|
|
38
|
+
string_key: 'value1',
|
|
39
|
+
symbol_key: 'value2'
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'handles empty hash' do
|
|
44
|
+
instance.setup_options({})
|
|
45
|
+
expect(instance.instance_variable_get(:@options)).to eq({})
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#reload_options' do
|
|
50
|
+
it 'merges new options with existing options' do
|
|
51
|
+
instance.setup_options({ key1: 'value1' })
|
|
52
|
+
instance.reload_options({ key2: 'value2' })
|
|
53
|
+
|
|
54
|
+
expect(instance.instance_variable_get(:@options)).to eq(
|
|
55
|
+
key1: 'value1',
|
|
56
|
+
key2: 'value2'
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'overwrites existing keys' do
|
|
61
|
+
instance.setup_options({ key1: 'old_value' })
|
|
62
|
+
instance.reload_options({ key1: 'new_value' })
|
|
63
|
+
|
|
64
|
+
expect(instance.instance_variable_get(:@options)[:key1]).to eq('new_value')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'handles string keys in reload' do
|
|
68
|
+
instance.setup_options({ key1: 'value1' })
|
|
69
|
+
instance.reload_options({ 'key2' => 'value2' })
|
|
70
|
+
|
|
71
|
+
expect(instance.instance_variable_get(:@options)).to include(
|
|
72
|
+
key1: 'value1',
|
|
73
|
+
key2: 'value2'
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#default_option' do
|
|
79
|
+
it 'sets default value when option does not exist' do
|
|
80
|
+
instance.setup_options({})
|
|
81
|
+
instance.default_option(:new_key, 'default_value')
|
|
82
|
+
|
|
83
|
+
expect(instance.instance_variable_get(:@options)[:new_key]).to eq('default_value')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'does not overwrite existing option' do
|
|
87
|
+
instance.setup_options({ existing_key: 'existing_value' })
|
|
88
|
+
instance.default_option(:existing_key, 'default_value')
|
|
89
|
+
|
|
90
|
+
expect(instance.instance_variable_get(:@options)[:existing_key]).to eq('existing_value')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'handles string name' do
|
|
94
|
+
instance.setup_options({})
|
|
95
|
+
instance.default_option('string_key', 'value')
|
|
96
|
+
|
|
97
|
+
expect(instance.instance_variable_get(:@options)[:string_key]).to eq('value')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'handles symbol name' do
|
|
101
|
+
instance.setup_options({})
|
|
102
|
+
instance.default_option(:symbol_key, 'value')
|
|
103
|
+
|
|
104
|
+
expect(instance.instance_variable_get(:@options)[:symbol_key]).to eq('value')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#required_option' do
|
|
109
|
+
it 'does not raise when required option exists' do
|
|
110
|
+
instance.setup_options({ required_key: 'value' })
|
|
111
|
+
expect { instance.required_option(:required_key) }.not_to raise_error
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'raises error when required option is missing' do
|
|
115
|
+
instance.setup_options({})
|
|
116
|
+
expect { instance.required_option(:missing_key) }.to raise_error(/missing_key is a required option/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'handles string name' do
|
|
120
|
+
instance.setup_options({ 'string_key' => 'value' })
|
|
121
|
+
expect { instance.required_option('string_key') }.not_to raise_error
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'handles multiple required options' do
|
|
125
|
+
instance.setup_options({ key1: 'value1', key2: 'value2' })
|
|
126
|
+
expect { instance.required_option([:key1, :key2]) }.not_to raise_error
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'raises error when any required option is missing' do
|
|
130
|
+
instance.setup_options({ key1: 'value1' })
|
|
131
|
+
expect { instance.required_option([:key1, :missing_key]) }.to raise_error(/missing_key is a required option/)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'handles single option as array' do
|
|
135
|
+
instance.setup_options({ key1: 'value1' })
|
|
136
|
+
expect { instance.required_option([:key1]) }.not_to raise_error
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'handles single option as symbol' do
|
|
140
|
+
instance.setup_options({ key1: 'value1' })
|
|
141
|
+
expect { instance.required_option(:key1) }.not_to raise_error
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe MailDaemon::Imap::Connection do
|
|
4
|
+
let(:options) do
|
|
5
|
+
{
|
|
6
|
+
host: 'imap.example.com',
|
|
7
|
+
username: 'user@example.com',
|
|
8
|
+
password: 'password',
|
|
9
|
+
port: 993,
|
|
10
|
+
ssl: true,
|
|
11
|
+
folder: 'inbox',
|
|
12
|
+
search_command: 'UNSEEN',
|
|
13
|
+
debug: false
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:imap_mock) { instance_double(Net::IMAP) }
|
|
18
|
+
let(:status_block) { proc { |status| } }
|
|
19
|
+
|
|
20
|
+
describe '#initialize' do
|
|
21
|
+
it 'sets up options' do
|
|
22
|
+
connection = described_class.new(options, &status_block)
|
|
23
|
+
expect(connection.instance_variable_get(:@options)).to include(
|
|
24
|
+
host: 'imap.example.com',
|
|
25
|
+
username: 'user@example.com',
|
|
26
|
+
password: 'password'
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'requires host option' do
|
|
31
|
+
expect { described_class.new({ username: 'user', password: 'pass' }, &status_block) }.to raise_error(/host is a required option/)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'requires username option' do
|
|
35
|
+
expect { described_class.new({ host: 'host', password: 'pass' }, &status_block) }.to raise_error(/username is a required option/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'requires password option' do
|
|
39
|
+
expect { described_class.new({ host: 'host', username: 'user' }, &status_block) }.to raise_error(/password is a required option/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'defaults port to 143' do
|
|
43
|
+
# Remove port from options to test default
|
|
44
|
+
opts = options.dup
|
|
45
|
+
opts.delete(:port)
|
|
46
|
+
connection = described_class.new(opts, &status_block)
|
|
47
|
+
expect(connection.instance_variable_get(:@options)[:port]).to eq(143)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'defaults folder to inbox' do
|
|
51
|
+
connection = described_class.new(options, &status_block)
|
|
52
|
+
expect(connection.instance_variable_get(:@options)[:folder]).to eq('inbox')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'defaults ssl to false' do
|
|
56
|
+
# Remove ssl from options to test default
|
|
57
|
+
opts = options.dup
|
|
58
|
+
opts.delete(:ssl)
|
|
59
|
+
connection = described_class.new(opts, &status_block)
|
|
60
|
+
expect(connection.instance_variable_get(:@options)[:ssl]).to eq(false)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'defaults sleep_time to 30' do
|
|
64
|
+
connection = described_class.new(options, &status_block)
|
|
65
|
+
expect(connection.instance_variable_get(:@options)[:sleep_time]).to eq(30)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'sets initial status to INITIALIZING' do
|
|
69
|
+
connection = described_class.new(options, &status_block)
|
|
70
|
+
expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::INITIALIZING)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#login' do
|
|
75
|
+
before do
|
|
76
|
+
allow(Net::IMAP).to receive(:new).and_return(imap_mock)
|
|
77
|
+
allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
|
|
78
|
+
allow(imap_mock).to receive(:login)
|
|
79
|
+
allow(imap_mock).to receive(:select)
|
|
80
|
+
allow(imap_mock).to receive(:disconnected?).and_return(false)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'creates IMAP connection' do
|
|
84
|
+
connection = described_class.new(options, &status_block)
|
|
85
|
+
# ssl_options defaults to false, so ssl: false is expected
|
|
86
|
+
expect(Net::IMAP).to receive(:new).with('imap.example.com', port: 993, ssl: false).and_return(imap_mock)
|
|
87
|
+
connection.login
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'checks for IDLE capability' do
|
|
91
|
+
connection = described_class.new(options, &status_block)
|
|
92
|
+
expect(imap_mock).to receive(:capability)
|
|
93
|
+
connection.login
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'sets idle_available when IDLE is supported' do
|
|
97
|
+
connection = described_class.new(options, &status_block)
|
|
98
|
+
connection.login
|
|
99
|
+
expect(connection.instance_variable_get(:@idle_available)).to be true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'sets idle_available to false when IDLE is not supported' do
|
|
103
|
+
allow(imap_mock).to receive(:capability).and_return(['IMAP4rev1'])
|
|
104
|
+
connection = described_class.new(options, &status_block)
|
|
105
|
+
connection.login
|
|
106
|
+
expect(connection.instance_variable_get(:@idle_available)).to be false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'logs in with username and password' do
|
|
110
|
+
connection = described_class.new(options, &status_block)
|
|
111
|
+
expect(imap_mock).to receive(:login).with('user@example.com', 'password')
|
|
112
|
+
connection.login
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'selects the inbox folder' do
|
|
116
|
+
connection = described_class.new(options, &status_block)
|
|
117
|
+
expect(imap_mock).to receive(:select).with('INBOX')
|
|
118
|
+
connection.login
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'sets status to LOGGED_ON after successful login' do
|
|
122
|
+
connection = described_class.new(options, &status_block)
|
|
123
|
+
connection.login
|
|
124
|
+
expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::LOGGED_ON)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'uses SSL options when provided' do
|
|
128
|
+
ssl_options = { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
129
|
+
opts = options.merge(ssl_options: ssl_options)
|
|
130
|
+
connection = described_class.new(opts, &status_block)
|
|
131
|
+
expect(Net::IMAP).to receive(:new).with('imap.example.com', port: 993, ssl: ssl_options).and_return(imap_mock)
|
|
132
|
+
connection.login
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe '#wait_for_messages' do
|
|
137
|
+
before do
|
|
138
|
+
allow(Net::IMAP).to receive(:new).and_return(imap_mock)
|
|
139
|
+
allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
|
|
140
|
+
allow(imap_mock).to receive(:login)
|
|
141
|
+
allow(imap_mock).to receive(:select)
|
|
142
|
+
allow(imap_mock).to receive(:disconnected?).and_return(false)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'fetches message IDs using search command' do
|
|
146
|
+
connection = described_class.new(options, &status_block)
|
|
147
|
+
connection.login
|
|
148
|
+
|
|
149
|
+
# Set idle_required to false BEFORE calling wait_for_messages to prevent infinite loop
|
|
150
|
+
connection.instance_variable_set(:@idle_required, false)
|
|
151
|
+
|
|
152
|
+
allow(imap_mock).to receive(:uid_search).and_return([1, 2, 3])
|
|
153
|
+
# Mock the structure: envelope[0][:attr]["RFC822"]
|
|
154
|
+
fetch_data = { attr: { 'RFC822' => 'message body' } }
|
|
155
|
+
allow(imap_mock).to receive(:uid_fetch).and_return([fetch_data])
|
|
156
|
+
|
|
157
|
+
# Mock idle to not block - it won't be called since idle_required is false, but just in case
|
|
158
|
+
allow(imap_mock).to receive(:idle) do |&block|
|
|
159
|
+
# Don't call the block to prevent blocking
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
expect(imap_mock).to receive(:uid_search).with('UNSEEN').at_least(:once)
|
|
163
|
+
connection.wait_for_messages { |msg| }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'yields fetched messages' do
|
|
167
|
+
connection = described_class.new(options, &status_block)
|
|
168
|
+
connection.login
|
|
169
|
+
|
|
170
|
+
# Set idle_required to false BEFORE calling wait_for_messages to prevent infinite loop
|
|
171
|
+
connection.instance_variable_set(:@idle_required, false)
|
|
172
|
+
|
|
173
|
+
message_body = 'email message content'
|
|
174
|
+
allow(imap_mock).to receive(:uid_search).and_return([1])
|
|
175
|
+
# Fix the structure to match what fetch_message expects: envelope[0][:attr]["RFC822"]
|
|
176
|
+
# The code does: envelope[0][:attr]["RFC822"]
|
|
177
|
+
# envelope is the return value of uid_fetch, which is an array
|
|
178
|
+
# envelope[0] is the first element, which should support [:attr] access
|
|
179
|
+
# envelope[0][:attr] should return a hash with "RFC822" key
|
|
180
|
+
fetch_data = { attr: { 'RFC822' => message_body } }
|
|
181
|
+
allow(imap_mock).to receive(:uid_fetch).with(1, ['RFC822']).and_return([fetch_data])
|
|
182
|
+
|
|
183
|
+
# Mock idle to not block (won't be called since idle_required is false)
|
|
184
|
+
allow(imap_mock).to receive(:idle) do |&block|
|
|
185
|
+
# Don't call the block to prevent blocking
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
yielded_message = nil
|
|
189
|
+
connection.wait_for_messages { |msg| yielded_message = msg }
|
|
190
|
+
|
|
191
|
+
expect(yielded_message).to eq(message_body)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'uses polling mode when IDLE is not available' do
|
|
195
|
+
allow(imap_mock).to receive(:capability).and_return(['IMAP4rev1'])
|
|
196
|
+
connection = described_class.new(options, &status_block)
|
|
197
|
+
connection.login
|
|
198
|
+
|
|
199
|
+
# Set idle_required to false BEFORE calling wait_for_messages to prevent infinite loop
|
|
200
|
+
connection.instance_variable_set(:@idle_required, false)
|
|
201
|
+
|
|
202
|
+
allow(imap_mock).to receive(:uid_search).and_return([])
|
|
203
|
+
|
|
204
|
+
expect(imap_mock).to receive(:uid_search).at_least(:once)
|
|
205
|
+
connection.wait_for_messages { |msg| }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe '#logout' do
|
|
210
|
+
before do
|
|
211
|
+
allow(Net::IMAP).to receive(:new).and_return(imap_mock)
|
|
212
|
+
allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
|
|
213
|
+
allow(imap_mock).to receive(:login)
|
|
214
|
+
allow(imap_mock).to receive(:select)
|
|
215
|
+
allow(imap_mock).to receive(:disconnected?).and_return(false)
|
|
216
|
+
allow(imap_mock).to receive(:logout)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'logs out from IMAP server' do
|
|
220
|
+
connection = described_class.new(options, &status_block)
|
|
221
|
+
connection.login
|
|
222
|
+
expect(imap_mock).to receive(:logout)
|
|
223
|
+
connection.logout
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'sets status to LOGGED_OFF' do
|
|
227
|
+
connection = described_class.new(options, &status_block)
|
|
228
|
+
connection.login
|
|
229
|
+
connection.logout
|
|
230
|
+
expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::LOGGED_OFF)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'does not logout if already disconnected' do
|
|
234
|
+
connection = described_class.new(options, &status_block)
|
|
235
|
+
connection.login
|
|
236
|
+
allow(imap_mock).to receive(:disconnected?).and_return(true)
|
|
237
|
+
expect(imap_mock).not_to receive(:logout)
|
|
238
|
+
connection.logout
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
describe '#disconnect' do
|
|
243
|
+
before do
|
|
244
|
+
allow(Net::IMAP).to receive(:new).and_return(imap_mock)
|
|
245
|
+
allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
|
|
246
|
+
allow(imap_mock).to receive(:login)
|
|
247
|
+
allow(imap_mock).to receive(:select)
|
|
248
|
+
allow(imap_mock).to receive(:disconnected?).and_return(false)
|
|
249
|
+
allow(imap_mock).to receive(:logout)
|
|
250
|
+
allow(imap_mock).to receive(:idle_done)
|
|
251
|
+
allow(imap_mock).to receive(:disconnect)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'disconnects from IMAP server' do
|
|
255
|
+
connection = described_class.new(options, &status_block)
|
|
256
|
+
connection.login
|
|
257
|
+
expect(imap_mock).to receive(:disconnect)
|
|
258
|
+
connection.disconnect
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it 'sets status to DISCONNECTED' do
|
|
262
|
+
connection = described_class.new(options, &status_block)
|
|
263
|
+
connection.login
|
|
264
|
+
connection.disconnect
|
|
265
|
+
expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::DISCONNECTED)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'handles errors during disconnect gracefully' do
|
|
269
|
+
connection = described_class.new(options, &status_block)
|
|
270
|
+
connection.login
|
|
271
|
+
allow(imap_mock).to receive(:disconnect).and_raise(StandardError.new('Connection error'))
|
|
272
|
+
|
|
273
|
+
expect { connection.disconnect }.not_to raise_error
|
|
274
|
+
expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::DISCONNECTED)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
describe '#running?' do
|
|
279
|
+
it 'returns false when not connected' do
|
|
280
|
+
connection = described_class.new(options, &status_block)
|
|
281
|
+
# @imap is nil until login is called
|
|
282
|
+
expect(connection.running?).to be false
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'returns true when connected' do
|
|
286
|
+
allow(Net::IMAP).to receive(:new).and_return(imap_mock)
|
|
287
|
+
allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
|
|
288
|
+
allow(imap_mock).to receive(:login)
|
|
289
|
+
allow(imap_mock).to receive(:select)
|
|
290
|
+
allow(imap_mock).to receive(:disconnected?).and_return(false)
|
|
291
|
+
|
|
292
|
+
connection = described_class.new(options, &status_block)
|
|
293
|
+
connection.login
|
|
294
|
+
expect(connection.running?).to be true
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
describe '#status' do
|
|
299
|
+
it 'returns current status' do
|
|
300
|
+
connection = described_class.new(options, &status_block)
|
|
301
|
+
status = connection.status
|
|
302
|
+
expect(status).to have_key(:status)
|
|
303
|
+
expect(status[:status]).to eq(MailDaemon::Imap::Statuses::INITIALIZING)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe MailDaemon::Imap::Statuses do
|
|
4
|
+
it 'defines INITIALIZING constant' do
|
|
5
|
+
expect(MailDaemon::Imap::Statuses::INITIALIZING).to eq('initializing')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'defines CONNECTING constant' do
|
|
9
|
+
expect(MailDaemon::Imap::Statuses::CONNECTING).to eq('connecting')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines CONNECTED constant' do
|
|
13
|
+
expect(MailDaemon::Imap::Statuses::CONNECTED).to eq('connected')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines LOGGING_ON constant' do
|
|
17
|
+
expect(MailDaemon::Imap::Statuses::LOGGING_ON).to eq('logging_on')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'defines LOGGED_ON constant' do
|
|
21
|
+
expect(MailDaemon::Imap::Statuses::LOGGED_ON).to eq('logged_on')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'defines LOGGING_OFF constant' do
|
|
25
|
+
expect(MailDaemon::Imap::Statuses::LOGGING_OFF).to eq('logging_off')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defines LOGGED_OFF constant' do
|
|
29
|
+
expect(MailDaemon::Imap::Statuses::LOGGED_OFF).to eq('logged_off')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'defines IDLEING constant' do
|
|
33
|
+
expect(MailDaemon::Imap::Statuses::IDLEING).to eq('ready')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'defines POLLING constant' do
|
|
37
|
+
expect(MailDaemon::Imap::Statuses::POLLING).to eq('ready')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'defines DISCONNECTING constant' do
|
|
41
|
+
expect(MailDaemon::Imap::Statuses::DISCONNECTING).to eq('disconnecting')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'defines DISCONNECTED constant' do
|
|
45
|
+
expect(MailDaemon::Imap::Statuses::DISCONNECTED).to eq('disconnected')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe MailDaemon::Imap::Watcher do
|
|
4
|
+
let(:options) do
|
|
5
|
+
{
|
|
6
|
+
host: 'imap.example.com',
|
|
7
|
+
username: 'user@example.com',
|
|
8
|
+
password: 'password',
|
|
9
|
+
account_code: 'test_account',
|
|
10
|
+
debug: false
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'sets options' do
|
|
16
|
+
watcher = described_class.new(options)
|
|
17
|
+
expect(watcher.instance_variable_get(:@options)).to eq(options)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#start' do
|
|
22
|
+
let(:connection) { instance_double(MailDaemon::Imap::Connection) }
|
|
23
|
+
|
|
24
|
+
it 'creates and logs in to connection' do
|
|
25
|
+
watcher = described_class.new(options)
|
|
26
|
+
# Connection.new receives options and a status notification block (the block passed to start)
|
|
27
|
+
expect(MailDaemon::Imap::Connection).to receive(:new).and_return(connection)
|
|
28
|
+
expect(connection).to receive(:login)
|
|
29
|
+
# Mock wait_for_messages to yield once then return to prevent infinite loop
|
|
30
|
+
# Note: wait_for_messages receives a block that yields messages
|
|
31
|
+
allow(connection).to receive(:wait_for_messages) do |&message_block|
|
|
32
|
+
message_block.call('message content') if message_block
|
|
33
|
+
# Set idle_required to false to exit any loops
|
|
34
|
+
connection.instance_variable_set(:@idle_required, false) if connection.respond_to?(:instance_variable_set)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Run in a thread with timeout to prevent hanging
|
|
38
|
+
thread = Thread.new do
|
|
39
|
+
watcher.start { |msg| }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sleep 0.1
|
|
43
|
+
thread.kill if thread.alive?
|
|
44
|
+
thread.join(0.1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'yields messages with type and mailbox info' do
|
|
48
|
+
watcher = described_class.new(options)
|
|
49
|
+
allow(MailDaemon::Imap::Connection).to receive(:new).and_return(connection)
|
|
50
|
+
allow(connection).to receive(:login)
|
|
51
|
+
# Mock wait_for_messages to yield once then return
|
|
52
|
+
allow(connection).to receive(:wait_for_messages) do |&block|
|
|
53
|
+
block.call('message content') if block
|
|
54
|
+
connection.instance_variable_set(:@idle_required, false) if connection.respond_to?(:instance_variable_set)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
yielded_data = nil
|
|
58
|
+
|
|
59
|
+
thread = Thread.new do
|
|
60
|
+
watcher.start do |data|
|
|
61
|
+
yielded_data = data
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sleep 0.1
|
|
66
|
+
thread.kill if thread.alive?
|
|
67
|
+
thread.join(0.1)
|
|
68
|
+
|
|
69
|
+
expect(yielded_data).to include(
|
|
70
|
+
type: 'incoming_email',
|
|
71
|
+
mailbox: options,
|
|
72
|
+
inbound_message: 'message content'
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#stop' do
|
|
78
|
+
it 'disconnects the connection' do
|
|
79
|
+
watcher = described_class.new(options)
|
|
80
|
+
connection = instance_double(MailDaemon::Imap::Connection)
|
|
81
|
+
watcher.instance_variable_set(:@connection, connection)
|
|
82
|
+
|
|
83
|
+
expect(connection).to receive(:disconnect)
|
|
84
|
+
watcher.stop
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe '#restart' do
|
|
89
|
+
it 'stops and starts the connection' do
|
|
90
|
+
watcher = described_class.new(options)
|
|
91
|
+
connection = instance_double(MailDaemon::Imap::Connection)
|
|
92
|
+
watcher.instance_variable_set(:@connection, connection)
|
|
93
|
+
|
|
94
|
+
expect(watcher).to receive(:stop)
|
|
95
|
+
expect(watcher).to receive(:start)
|
|
96
|
+
watcher.restart
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#running?' do
|
|
101
|
+
it 'returns false when connection is nil' do
|
|
102
|
+
watcher = described_class.new(options)
|
|
103
|
+
expect(watcher.running?).to be false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'delegates to connection running? method' do
|
|
107
|
+
watcher = described_class.new(options)
|
|
108
|
+
connection = instance_double(MailDaemon::Imap::Connection, running?: true)
|
|
109
|
+
watcher.instance_variable_set(:@connection, connection)
|
|
110
|
+
|
|
111
|
+
expect(watcher.running?).to be true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe '#mailbox' do
|
|
116
|
+
it 'returns mailbox from options' do
|
|
117
|
+
watcher = described_class.new(options.merge(mailbox: { name: 'inbox' }))
|
|
118
|
+
expect(watcher.mailbox).to eq({ name: 'inbox' })
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#to_s' do
|
|
123
|
+
it 'returns account_code and username' do
|
|
124
|
+
watcher = described_class.new(options)
|
|
125
|
+
expect(watcher.to_s).to eq('test_account/user@example.com')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|