imapcli 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +117 -0
- data/Guardfile +65 -0
- data/README.md +227 -0
- data/Rakefile +45 -0
- data/bin/imapcli +131 -0
- data/imapcli.gemspec +21 -0
- data/imapcli.rdoc +5 -0
- data/lib/imapcli/client.rb +226 -0
- data/lib/imapcli/command.rb +100 -0
- data/lib/imapcli/mailbox.rb +108 -0
- data/lib/imapcli/version.rb +3 -0
- data/lib/imapcli.rb +4 -0
- data/spec/lib/imapcli/client_spec.rb +70 -0
- data/spec/lib/imapcli/mailbox_spec.rb +21 -0
- data/spec/spec_helper.rb +102 -0
- metadata +111 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
module Imapcli
|
2
|
+
# Wrapper for Net::IMAP
|
3
|
+
class Client
|
4
|
+
require 'net/imap'
|
5
|
+
require 'filesize'
|
6
|
+
require 'descriptive_statistics'
|
7
|
+
|
8
|
+
attr_accessor :port, :user, :pass
|
9
|
+
attr_reader :responses
|
10
|
+
|
11
|
+
## Initializs the Client class.
|
12
|
+
##
|
13
|
+
## +server_with_optional_port+ is the server's domain name; the port may be
|
14
|
+
## added following a colon. Default port is 993.
|
15
|
+
## +user+ is the user (account) name to log into the server.
|
16
|
+
## +pass+ is the password to log into the server.
|
17
|
+
def initialize(server_with_optional_port, user, pass)
|
18
|
+
@port = 993 # default
|
19
|
+
self.server, @user, @pass = server_with_optional_port, user, pass
|
20
|
+
clear_log
|
21
|
+
end
|
22
|
+
|
23
|
+
# Attribute reader for the server domain name
|
24
|
+
def server
|
25
|
+
@server
|
26
|
+
end
|
27
|
+
|
28
|
+
# Attribute writer for the server domain name; a port may be appended with
|
29
|
+
# a colon.
|
30
|
+
#
|
31
|
+
# If no port is appended, the default port (993) will be used.
|
32
|
+
def server=(server_with_optional_port)
|
33
|
+
match = server_with_optional_port.match('^([^:]+):(\d+)$')
|
34
|
+
if match
|
35
|
+
@server = match[1]
|
36
|
+
@port = match[2].to_i
|
37
|
+
else
|
38
|
+
@server = server_with_optional_port
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Perform basic sanity check on server name
|
43
|
+
#
|
44
|
+
# Note that a propery regex for an FQDN is hard to achieve.
|
45
|
+
# See https://stackoverflow.com/a/106223/270712 and elsewhere.
|
46
|
+
def server_valid?
|
47
|
+
@server.match? '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
|
48
|
+
end
|
49
|
+
|
50
|
+
# Perform *very* basic sanity check on user name
|
51
|
+
#
|
52
|
+
def user_valid?
|
53
|
+
@user&.length > 0
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns true if both server and user name are valid.
|
57
|
+
def valid?
|
58
|
+
server_valid? && user_valid?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Clears the server response log
|
62
|
+
def clear_log
|
63
|
+
@log = []
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the last response from the server
|
67
|
+
def last_response
|
68
|
+
@log.last
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a connection to the server.
|
72
|
+
#
|
73
|
+
# The value is cached.
|
74
|
+
def connection
|
75
|
+
@connection ||= Net::IMAP.new(@server, @port, true)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Logs in to the server.
|
79
|
+
#
|
80
|
+
# Returns true if login was successful, false if not (e..g, invalid
|
81
|
+
# credentials).
|
82
|
+
def login
|
83
|
+
raise('no connection to a server') unless connection
|
84
|
+
begin
|
85
|
+
response_ok? connection.login(@user, @pass)
|
86
|
+
rescue Net::IMAP::NoResponseError => error
|
87
|
+
log_error error
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Logs out of the server.
|
92
|
+
def logout
|
93
|
+
# access instance variable to avoid creating a new connection
|
94
|
+
@connection.logout if @connection
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns the server's greeting (which may reveal the server software name
|
98
|
+
# such as 'Dovecot').
|
99
|
+
def greeting
|
100
|
+
query_server { connection.greeting.data.text.strip }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the server's capabilities.
|
104
|
+
def capability
|
105
|
+
@capability ||= query_server { connection.capability }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the character that is used to separate nested mailbox names.
|
109
|
+
def separator
|
110
|
+
@separator ||= query_server { connection.list('', '')[0].delim }
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns true if the server supports the IMAP QUOTA extension.
|
114
|
+
def supports_quota
|
115
|
+
capability.include? 'QUOTA'
|
116
|
+
end
|
117
|
+
|
118
|
+
# If the server +supports_quota+, returns an array containing the current
|
119
|
+
# usage (in kiB), the total quota (in kiB), and the percent usage.
|
120
|
+
def quota
|
121
|
+
if supports_quota
|
122
|
+
@quota ||= begin
|
123
|
+
info = query_server { @connection.getquotaroot('INBOX')[1] }
|
124
|
+
percent = info.quota.to_i > 0 ? info.usage.to_i.fdiv(info.quota.to_i) * 100 : nil
|
125
|
+
[ info.usage, info.quota, percent ]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns an array of message indexes for a mailbox.
|
131
|
+
#
|
132
|
+
# The value is currently NOT cached.
|
133
|
+
def messages(mailbox)
|
134
|
+
query_server { connection.examine(mailbox) }
|
135
|
+
query_server { connection.search('ALL') }
|
136
|
+
end
|
137
|
+
|
138
|
+
# Examines a mailbox and returns statistics about the messages in it.
|
139
|
+
#
|
140
|
+
# Returns an array with the following keys:
|
141
|
+
# * :count: Total count of messages.
|
142
|
+
# * :size: Total size of all messages in bytes.
|
143
|
+
# * :min: Size of the smallest message.
|
144
|
+
# * :q1: First quartile of message sizes.
|
145
|
+
# * :median: Median of message sizes.
|
146
|
+
# * :q3: Third quartile of messages sizes.
|
147
|
+
# * :max: Size of largest message.
|
148
|
+
def examine(mailbox)
|
149
|
+
# Could use the EXAMINE command to get the number of messages in a mailbox,
|
150
|
+
# but we need to retrieve an array of message indexes anyway (to compute
|
151
|
+
# the total mailbox size), so we can save one roundtrip to the server.
|
152
|
+
# query_server { connection.examine(mailbox) }
|
153
|
+
# total = connection.responses['EXISTS'][0]
|
154
|
+
# unseen = query_server { connection.search('UNSEEN') }.length
|
155
|
+
messages = messages(mailbox)
|
156
|
+
count = messages.length
|
157
|
+
sizes = query_server { connection.fetch(messages, 'RFC822.SIZE').map { |f| f.attr['RFC822.SIZE'] }.sort }
|
158
|
+
{
|
159
|
+
count: count,
|
160
|
+
size: convert_bytes(sizes.sum),
|
161
|
+
min: convert_bytes(sizes.first),
|
162
|
+
q1: convert_bytes(sizes.percentile(25)),
|
163
|
+
median: convert_bytes(sizes.median),
|
164
|
+
q3: convert_bytes(sizes.percentile(75)),
|
165
|
+
max: convert_bytes(sizes.last)
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
# Collects stats for all mailboxes recursively.
|
170
|
+
def collect_stats
|
171
|
+
mailbox_tree.collect_stats(self)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Gets a list of Net::IMAP::MailboxList items, one for each mailbox.
|
175
|
+
#
|
176
|
+
# The value is cached.
|
177
|
+
def mailboxes
|
178
|
+
@mailboxes ||= query_server { @connection.list('', '*') }
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns a tree of +Imapcli::Mailbox+ objects.
|
182
|
+
#
|
183
|
+
# The value is cached.
|
184
|
+
def mailbox_tree
|
185
|
+
@mailbox_tree ||= Mailbox.new(mailboxes)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Attempts to locate a given +mailbox+ in the +mailbox_tree+.
|
189
|
+
#
|
190
|
+
# Returns nil if the mailbox is not found.
|
191
|
+
def find_mailbox(mailbox)
|
192
|
+
mailbox_tree.find_sub_mailbox(mailbox, separator)
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def response_ok?(response)
|
198
|
+
@log << response
|
199
|
+
response.name == 'OK'
|
200
|
+
end
|
201
|
+
|
202
|
+
def log_error(error)
|
203
|
+
@log << error
|
204
|
+
false
|
205
|
+
end
|
206
|
+
|
207
|
+
# Wrapper function that can be used to execute code that queries the server.
|
208
|
+
#
|
209
|
+
# This function ensures that there is a valid +connection+ and raises an
|
210
|
+
# error if not. The code that queries the server must be contained in the
|
211
|
+
# +block+, and the +block+'s return value is returned by this function.
|
212
|
+
# The +connection+'s responses are logged.
|
213
|
+
def query_server(&block)
|
214
|
+
raise('no connection to a server') unless connection
|
215
|
+
result = yield
|
216
|
+
@log << connection.responses
|
217
|
+
result
|
218
|
+
end
|
219
|
+
|
220
|
+
# Converts a number of bytes to kiB.
|
221
|
+
def convert_bytes(bytes)
|
222
|
+
bytes.fdiv(1024).round
|
223
|
+
end
|
224
|
+
|
225
|
+
end # class Client
|
226
|
+
end # module Imapcli
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Imapcli
|
2
|
+
# Provides entry points for Imapcli.
|
3
|
+
#
|
4
|
+
# Most of the methods in this class return
|
5
|
+
class Command
|
6
|
+
def initialize(client)
|
7
|
+
raise 'Imapcli::Client is required' unless client
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
# Checks if the server accepts the login with the given credentials.
|
12
|
+
#
|
13
|
+
# Returns true if successful, false if not.
|
14
|
+
def check
|
15
|
+
@client.login
|
16
|
+
end
|
17
|
+
|
18
|
+
# Collects basic information about the server.
|
19
|
+
#
|
20
|
+
# The block is called repeatedly with informative messages.
|
21
|
+
# If login is not successful, an error will be raised.
|
22
|
+
def info
|
23
|
+
perform do |output|
|
24
|
+
output << "greeting: #{@client.greeting}"
|
25
|
+
output << "capability: #{@client.capability.join(' ')}"
|
26
|
+
output << "hierarchy separator: #{@client.separator}"
|
27
|
+
if @client.supports_quota
|
28
|
+
usage = Filesize.from(@client.quota[0] + ' kB').pretty
|
29
|
+
available = Filesize.from(@client.quota[1] + ' kB').pretty
|
30
|
+
output << "quota: #{usage} used, #{available} available (#{@client.quota[2].round(1)}%)"
|
31
|
+
else
|
32
|
+
output << "quota: IMAP QUOTA extension not supported by this server"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def list
|
38
|
+
perform do |output|
|
39
|
+
@client.collect_stats
|
40
|
+
output << "mailboxes (folders) tree:"
|
41
|
+
output += traverse_mailbox_tree @client.mailbox_tree, 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def stats(mailboxes)
|
46
|
+
perform do |output|
|
47
|
+
mailboxes.each do |name|
|
48
|
+
if mailbox = @client.find_mailbox(name)
|
49
|
+
mailbox.collect_stats(@client)
|
50
|
+
output << [
|
51
|
+
mailbox.full_name,
|
52
|
+
mailbox.stats[:count],
|
53
|
+
format_kib(mailbox.stats[:size]),
|
54
|
+
format_kib(mailbox.stats[:min]),
|
55
|
+
format_kib(mailbox.stats[:q1]),
|
56
|
+
format_kib(mailbox.stats[:median]),
|
57
|
+
format_kib(mailbox.stats[:q3]),
|
58
|
+
format_kib(mailbox.stats[:max])
|
59
|
+
]
|
60
|
+
else
|
61
|
+
output << [ self.class.unknown_mailbox_prefix + name ] + Array.new(7, '---')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.unknown_mailbox_prefix
|
68
|
+
'!!! '
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def perform
|
74
|
+
output = []
|
75
|
+
if @client.login
|
76
|
+
yield output
|
77
|
+
else
|
78
|
+
raise 'unable to log into server'
|
79
|
+
end
|
80
|
+
output
|
81
|
+
end
|
82
|
+
|
83
|
+
def traverse_mailbox_tree(mailbox, depth = 0)
|
84
|
+
output = []
|
85
|
+
if mailbox.has_children?
|
86
|
+
indent = (' ' * depth) || ''
|
87
|
+
mailbox.children.each do |child|
|
88
|
+
output << indent + '- ' + child.name
|
89
|
+
output += traverse_mailbox_tree child, depth + 1
|
90
|
+
end
|
91
|
+
end
|
92
|
+
output
|
93
|
+
end
|
94
|
+
|
95
|
+
def format_kib(kib)
|
96
|
+
kib.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse + ' kiB'.freeze
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Imapcli
|
2
|
+
# In IMAP speak, a mailbox is what one would commonly call a 'folder'
|
3
|
+
class Mailbox
|
4
|
+
attr_reader :level, :children, :imap_mailbox_list, :name, :stats
|
5
|
+
|
6
|
+
# Creates a new root Mailbox object and optionally adds sub mailboxes from
|
7
|
+
# an array of Net::IMAP::MailboxList items.
|
8
|
+
def initialize(mailbox_list_items = nil)
|
9
|
+
@level = 0
|
10
|
+
@children = {}
|
11
|
+
add_mailbox_list(mailbox_list_items) if mailbox_list_items
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](mailbox)
|
15
|
+
@children[mailbox]
|
16
|
+
end
|
17
|
+
|
18
|
+
def full_name
|
19
|
+
imap_mailbox_list&.name
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_children?
|
23
|
+
@children.length > 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def children
|
27
|
+
@children.values
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add a list of mailboxes as returned by Net::IMAP#list.
|
31
|
+
def add_mailbox_list(array_of_mailbox_list_items)
|
32
|
+
array_of_mailbox_list_items.sort_by { |m| m.name.downcase }.each { |i| add_mailbox i }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds a sub mailbox designated by the +name+ of a Net::IMAP::MailboxList.
|
36
|
+
def add_mailbox(imap_mailbox_list, options = {})
|
37
|
+
return unless imap_mailbox_list&.name&.length > 0
|
38
|
+
recursive_add(0, imap_mailbox_list, imap_mailbox_list.name, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Attempts to locate and retrieve a sub mailbox.
|
42
|
+
#
|
43
|
+
# Returns nil of none exists with the given name.
|
44
|
+
# Name must be relative to the current mailbox.
|
45
|
+
def find_sub_mailbox(relative_name, delimiter)
|
46
|
+
if relative_name
|
47
|
+
sub_mailbox_name, subs_subs = relative_name.split(delimiter, 2)
|
48
|
+
if sub_mailbox = @children[sub_mailbox_name]
|
49
|
+
sub_mailbox.find_sub_mailbox(subs_subs, delimiter)
|
50
|
+
else
|
51
|
+
nil # no matching sub mailbox found, stop searching the tree
|
52
|
+
end
|
53
|
+
else
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Collects statistics for this mailbox.
|
59
|
+
#
|
60
|
+
# +connection+ must be a Net::IMAP object
|
61
|
+
def collect_stats(client)
|
62
|
+
if full_name # proceed only if this is a mailbox of its own
|
63
|
+
@stats = client.examine(full_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Collects statistics for this mailbox and all of its children.
|
68
|
+
#
|
69
|
+
# +connection+ must be a Net::IMAP object
|
70
|
+
def collect_stats_recursively(connection)
|
71
|
+
collect_stats(connection)
|
72
|
+
@children.values.each { |child| child.collect_stats_recursively(connection) }
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def level=(level)
|
78
|
+
@level = level
|
79
|
+
end
|
80
|
+
|
81
|
+
def name=(name)
|
82
|
+
@name = name
|
83
|
+
end
|
84
|
+
|
85
|
+
def recursive_add(level, imap_mailbox_list, relative_name = nil, options = {})
|
86
|
+
delimiter = options[:delimiter] || imap_mailbox_list.delim
|
87
|
+
if relative_name
|
88
|
+
sub_mailbox_name, subs_subs = relative_name.split(delimiter, 2)
|
89
|
+
if options[:case_insensitive] || (level == 0 && relative_name.upcase == 'INBOX')
|
90
|
+
key = sub_mailbox_name.upcase
|
91
|
+
else
|
92
|
+
key = sub_mailbox_name
|
93
|
+
end
|
94
|
+
# Create a new mailbox if there does not exist one by the name
|
95
|
+
unless sub_mailbox = @children[key]
|
96
|
+
sub_mailbox = Mailbox.new
|
97
|
+
sub_mailbox.level = level
|
98
|
+
sub_mailbox.name = sub_mailbox_name
|
99
|
+
@children[key] = sub_mailbox
|
100
|
+
end
|
101
|
+
sub_mailbox.recursive_add(level + 1, imap_mailbox_list, subs_subs, options)
|
102
|
+
else # no more sub mailboxes: we've reached the last of the children
|
103
|
+
@imap_mailbox_list = imap_mailbox_list
|
104
|
+
self
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/imapcli.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'imapcli'
|
2
|
+
require 'dotenv'
|
3
|
+
|
4
|
+
RSpec.describe Imapcli::Client do
|
5
|
+
|
6
|
+
context 'with mock credentials for a nonexistent server' do
|
7
|
+
let(:client) { Imapcli::Client.new('imap.example.com', 'username', 'password') }
|
8
|
+
it 'knows when a server name is invalid' do
|
9
|
+
client.server = 'i n v a l i d'
|
10
|
+
expect(client.server_valid?).to eq false
|
11
|
+
end
|
12
|
+
it 'knows when a server name is valid' do
|
13
|
+
client.server = 'imap.gmail.com'
|
14
|
+
expect(client.server_valid?).to eq true
|
15
|
+
end
|
16
|
+
it 'knows when a user name is invalid' do
|
17
|
+
client.user = ''
|
18
|
+
expect(client.user_valid?).to eq false
|
19
|
+
end
|
20
|
+
it 'knows when a user name is valid' do
|
21
|
+
client.user = 'bovender@example.com'
|
22
|
+
expect(client.user_valid?).to eq true
|
23
|
+
end
|
24
|
+
it 'uses port 993 by default' do
|
25
|
+
expect(client.port).to eq 993
|
26
|
+
end
|
27
|
+
it 'extracts a port from the server info' do
|
28
|
+
client.server = 'imap.example.com:143'
|
29
|
+
expect(client.port).to eq 143
|
30
|
+
end
|
31
|
+
it 'extracts a server from the server string when a port is appended' do
|
32
|
+
client.server = 'imap.example.com:143'
|
33
|
+
expect(client.server).to eq 'imap.example.com'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with valid credentials for an actual server' do
|
38
|
+
before :all do
|
39
|
+
Dotenv.load
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:client) { Imapcli::Client.new(ENV['IMAP_SERVER'], ENV['IMAP_USER'], ENV['IMAP_PASS']) }
|
43
|
+
|
44
|
+
it 'the IMAP_SERVER variable must be set' do
|
45
|
+
expect(ENV['IMAP_SERVER']).to_not eq(nil)
|
46
|
+
end
|
47
|
+
it 'the IMAP_USER variable must be set' do
|
48
|
+
expect(ENV['IMAP_USER']).to_not eq(nil)
|
49
|
+
end
|
50
|
+
it 'the IMAP_PASS variable must be set' do
|
51
|
+
expect(ENV['IMAP_PASS']).to_not eq(nil)
|
52
|
+
end
|
53
|
+
it 'successfully logs in to the server' do
|
54
|
+
expect(client.login).to eq true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with invalid credentials for an actual server' do
|
59
|
+
before :all do
|
60
|
+
Dotenv.load
|
61
|
+
end
|
62
|
+
|
63
|
+
let(:client) { Imapcli::Client.new(ENV['IMAP_SERVER'], ENV['IMAP_USER'], ENV['IMAP_PASS'] + 'invalid') }
|
64
|
+
|
65
|
+
it 'cannot log in to the server' do
|
66
|
+
expect(client.login).to eq false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'imapcli'
|
2
|
+
require 'net/imap'
|
3
|
+
|
4
|
+
RSpec.describe Imapcli::Mailbox do
|
5
|
+
# it 'parses a mailbox list' do
|
6
|
+
# mailbox_list = Net::IMAP::MailboxList.new(attr: nil, delim: '/', name: 'Root/Subfolder')
|
7
|
+
# mailbox_tree = Imapcli::MailboxTree.new(mailbox_list)
|
8
|
+
# expect(mailbox_tree.tree.length).to eq 1
|
9
|
+
# end
|
10
|
+
it 'returns nil if a given sub mailbox does not exist' do
|
11
|
+
mailbox = Imapcli::Mailbox.new
|
12
|
+
expect(mailbox.find_sub_mailbox('INBOX.does.not.exist', '.')).to eq nil
|
13
|
+
end
|
14
|
+
it 'adds and retrieves an existing sub mailbox' do
|
15
|
+
name = 'Root/Subfolder/Subsubfolder'
|
16
|
+
imap_mailbox_list = Net::IMAP::MailboxList.new(nil, '/', name)
|
17
|
+
mailbox = Imapcli::Mailbox.new
|
18
|
+
mailbox.add_mailbox(imap_mailbox_list)
|
19
|
+
expect(mailbox.find_sub_mailbox(name, '/').imap_mailbox_list).to eq imap_mailbox_list
|
20
|
+
end
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
4
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
5
|
+
# files.
|
6
|
+
#
|
7
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
8
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
9
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
10
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
11
|
+
# a separate helper file that requires the additional dependencies and performs
|
12
|
+
# the additional setup, and require it from the spec files that actually need
|
13
|
+
# it.
|
14
|
+
#
|
15
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
16
|
+
require 'pry'
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
# rspec-expectations config goes here. You can use an alternate
|
20
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
21
|
+
# assertions if you prefer.
|
22
|
+
config.expect_with :rspec do |expectations|
|
23
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
24
|
+
# and `failure_message` of custom matchers include text for helper methods
|
25
|
+
# defined using `chain`, e.g.:
|
26
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
27
|
+
# # => "be bigger than 2 and smaller than 4"
|
28
|
+
# ...rather than:
|
29
|
+
# # => "be bigger than 2"
|
30
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
34
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
35
|
+
config.mock_with :rspec do |mocks|
|
36
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
37
|
+
# a real object. This is generally recommended, and will default to
|
38
|
+
# `true` in RSpec 4.
|
39
|
+
mocks.verify_partial_doubles = true
|
40
|
+
end
|
41
|
+
|
42
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
43
|
+
# have no way to turn it off -- the option exists only for backwards
|
44
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
45
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
46
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
47
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
48
|
+
|
49
|
+
# The settings below are suggested to provide a good initial experience
|
50
|
+
# with RSpec, but feel free to customize to your heart's content.
|
51
|
+
=begin
|
52
|
+
# This allows you to limit a spec run to individual examples or groups
|
53
|
+
# you care about by tagging them with `:focus` metadata. When nothing
|
54
|
+
# is tagged with `:focus`, all examples get run. RSpec also provides
|
55
|
+
# aliases for `it`, `describe`, and `context` that include `:focus`
|
56
|
+
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
57
|
+
config.filter_run_when_matching :focus
|
58
|
+
|
59
|
+
# Allows RSpec to persist some state between runs in order to support
|
60
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
61
|
+
# you configure your source control system to ignore this file.
|
62
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
63
|
+
|
64
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
65
|
+
# recommended. For more details, see:
|
66
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
67
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
68
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
69
|
+
config.disable_monkey_patching!
|
70
|
+
|
71
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
72
|
+
# be too noisy due to issues in dependencies.
|
73
|
+
config.warnings = true
|
74
|
+
|
75
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
76
|
+
# file, and it's useful to allow more verbose output when running an
|
77
|
+
# individual spec file.
|
78
|
+
if config.files_to_run.one?
|
79
|
+
# Use the documentation formatter for detailed output,
|
80
|
+
# unless a formatter has already been configured
|
81
|
+
# (e.g. via a command-line flag).
|
82
|
+
config.default_formatter = "doc"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Print the 10 slowest examples and example groups at the
|
86
|
+
# end of the spec run, to help surface which specs are running
|
87
|
+
# particularly slow.
|
88
|
+
config.profile_examples = 10
|
89
|
+
|
90
|
+
# Run specs in random order to surface order dependencies. If you find an
|
91
|
+
# order dependency and want to debug it, you can fix the order by providing
|
92
|
+
# the seed, which is printed after each run.
|
93
|
+
# --seed 1234
|
94
|
+
config.order = :random
|
95
|
+
|
96
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
97
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
98
|
+
# test failures related to randomization by passing the same `--seed` value
|
99
|
+
# as the one that triggered the failure.
|
100
|
+
Kernel.srand config.seed
|
101
|
+
=end
|
102
|
+
end
|