eyemap 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/eyemap.rb +7 -0
- data/lib/eyemap/drivers/auto.rb +0 -0
- data/lib/eyemap/drivers/courier.rb +12 -0
- data/lib/eyemap/drivers/courier_folder.rb +13 -0
- data/lib/eyemap/drivers/dovecot.rb +12 -0
- data/lib/eyemap/drivers/dovecot_folder.rb +34 -0
- data/lib/eyemap/exception.rb +8 -0
- data/lib/eyemap/eyemap.rb +238 -0
- data/lib/eyemap/folder.rb +211 -0
- data/lib/eyemap/message.rb +99 -0
- data/lib/eyemap/search.rb +2 -0
- data/test/activeimap.rb +160 -0
- data/test/test_message +6 -0
- metadata +66 -0
data/lib/eyemap.rb
ADDED
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class EyeMap::Folder::Courier < EyeMap::Folder
|
2
|
+
|
3
|
+
def delete
|
4
|
+
# courier has an interesting situation where it does not
|
5
|
+
# allow the currently selected mailbox to be deleted.
|
6
|
+
# until I'm positive this is "normal" behavior I'm going to keep it
|
7
|
+
# here.
|
8
|
+
|
9
|
+
@driver.conn.select("INBOX")
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class EyeMap::Folder::Dovecot < EyeMap::Folder
|
2
|
+
|
3
|
+
def create
|
4
|
+
raise EyeMap::Exception::DriverIncapable.new("Dovecot cannot handle inferior folders.")
|
5
|
+
end
|
6
|
+
|
7
|
+
def folder(folder_name)
|
8
|
+
raise EyeMap::Exception::DriverIncapable.new("Dovecot cannot handle inferior folders.")
|
9
|
+
end
|
10
|
+
|
11
|
+
def subfolders
|
12
|
+
list = nil
|
13
|
+
|
14
|
+
if @folder_name.length > 0
|
15
|
+
list = @driver.conn.list("", "#{@folder_name}#{@driver[:delimiter]}%")
|
16
|
+
else
|
17
|
+
list = @driver.conn.list("", "%")
|
18
|
+
end
|
19
|
+
|
20
|
+
retval = []
|
21
|
+
|
22
|
+
if list
|
23
|
+
list.each do |f|
|
24
|
+
retval.push(@driver[:folder_class].
|
25
|
+
new(f.name, @driver, f.delim,
|
26
|
+
# deep copy
|
27
|
+
Marshal.load(Marshal.dump(f.attr))))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
return retval
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems'
|
5
|
+
rescue LoadError => e
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'active_support'
|
9
|
+
|
10
|
+
#
|
11
|
+
# EyeMap - an Objective interface to IMAP
|
12
|
+
#
|
13
|
+
# Those who have worked with IMAP should know that the servers that
|
14
|
+
# implement it have a world of hurt coming to them, and that's why
|
15
|
+
# they don't implement every single part of the spec right. There are
|
16
|
+
# too many SHOULD's and not enough MUST's.
|
17
|
+
#
|
18
|
+
# Anyways, our solution to this model is pretty simple. Provide
|
19
|
+
# drivers for the IMAP servers, which control their quirks, and
|
20
|
+
# present a unified interface in which the user does not have to care
|
21
|
+
# about the nasty underpinnings and can just get work done.
|
22
|
+
#
|
23
|
+
# EyeMap is a part of the Re: Mail project.
|
24
|
+
#
|
25
|
+
# To connect to an IMAP store, use the EyeMap.connect() call.
|
26
|
+
#
|
27
|
+
# To figure out what to do after you've got your connection, look at
|
28
|
+
# the methods in EyeMap::Driver.
|
29
|
+
#
|
30
|
+
|
31
|
+
class EyeMap
|
32
|
+
|
33
|
+
#
|
34
|
+
# EyeMap::Driver - Top level calls for IMAP connections.
|
35
|
+
#
|
36
|
+
# <documentation about implementing a driver goes here>
|
37
|
+
#
|
38
|
+
|
39
|
+
class Driver
|
40
|
+
protected
|
41
|
+
|
42
|
+
#
|
43
|
+
# Assign a driver capability in key/value format.
|
44
|
+
#
|
45
|
+
|
46
|
+
def []=(key, value)
|
47
|
+
@capabilities[key] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
public
|
51
|
+
|
52
|
+
#
|
53
|
+
# Create a new driver object. This really shouldn't be called by
|
54
|
+
# itself, but as a super() method, as it just fills in defaults
|
55
|
+
# intended to be overridden.
|
56
|
+
#
|
57
|
+
|
58
|
+
def initialize(args)
|
59
|
+
args = args[0]
|
60
|
+
|
61
|
+
if ! args.include? :user or
|
62
|
+
! args.include? :password or
|
63
|
+
! args.include? :host
|
64
|
+
|
65
|
+
raise EyeMap::Exception::BadCall.new("User, Password and Host must be provided to connect")
|
66
|
+
|
67
|
+
end
|
68
|
+
@capabilities = EyeMap::Capabilities.new
|
69
|
+
self[:driver_class] = args[:driver_class]
|
70
|
+
self[:driver] = args[:driver]
|
71
|
+
self[:message_class] = EyeMap::Message
|
72
|
+
self[:folder_class] = EyeMap::Folder
|
73
|
+
self[:delimiter] = '/'
|
74
|
+
self[:user] = args[:user]
|
75
|
+
self[:password] = args[:password]
|
76
|
+
self[:host] = args[:host]
|
77
|
+
self[:auth_mech] = 'LOGIN' # for now
|
78
|
+
self[:ssl] = !!args[:ssl]
|
79
|
+
self[:verify_ssl] = args[:ssl] ? !!args[:verify_ssl] : false
|
80
|
+
self[:cert] = args[:cert]
|
81
|
+
|
82
|
+
if ! args[:port]
|
83
|
+
args[:port] = args[:ssl] ? 993 : 143
|
84
|
+
end
|
85
|
+
|
86
|
+
self[:port] = args[:port]
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Fetch a capability. Capabilities are a struct, but are presented
|
92
|
+
# in hash form. See EyeMap::Capabilities for more information.
|
93
|
+
#
|
94
|
+
|
95
|
+
def [](key)
|
96
|
+
return @capabilities[key]
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Creates a new folder with the name specified.
|
101
|
+
#
|
102
|
+
|
103
|
+
def create(folder_name)
|
104
|
+
self.conn.create(folder_name)
|
105
|
+
self[:folder_class].new(folder_name, self, self[:delimiter])
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Get an EyeMap::Folder (or derivative) object for the name of
|
110
|
+
# the folder in question. Requires the full folder name.
|
111
|
+
#
|
112
|
+
|
113
|
+
def folder(folder_name=nil)
|
114
|
+
return self[:folder_class].
|
115
|
+
new(folder_name,
|
116
|
+
self,
|
117
|
+
self[:delimiter])
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Get the underlying Net::IMAP connection.
|
122
|
+
#
|
123
|
+
|
124
|
+
def conn
|
125
|
+
@conn
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Returns true if the connection is still alive.
|
130
|
+
#
|
131
|
+
|
132
|
+
def connected?
|
133
|
+
! @conn.disconnected?
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Initiates a connection (or reconnection) to the server.
|
138
|
+
#
|
139
|
+
|
140
|
+
def connect
|
141
|
+
if @conn and connected?
|
142
|
+
begin
|
143
|
+
@conn.authenticate("LOGIN", self[:user], self[:password]) unless
|
144
|
+
@conn.login(self[:user], self[:password])
|
145
|
+
rescue Exception => e
|
146
|
+
end
|
147
|
+
|
148
|
+
else
|
149
|
+
@conn = Net::IMAP.new(self[:host], self[:port],
|
150
|
+
self[:ssl], self[:cert],
|
151
|
+
self[:verify_ssl])
|
152
|
+
connect() # recurse
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
#
|
157
|
+
# Disconnect from the server
|
158
|
+
#
|
159
|
+
|
160
|
+
def disconnect
|
161
|
+
@conn.disconnect
|
162
|
+
end
|
163
|
+
|
164
|
+
end # Driver
|
165
|
+
|
166
|
+
#
|
167
|
+
# Use a driver to connect to an IMAP store.
|
168
|
+
#
|
169
|
+
# The arguments here are a collection of EyeMap::Capabilities
|
170
|
+
# items. If there are any driver-specific items, those will be noted
|
171
|
+
# for that driver. Please read the documentation for both.
|
172
|
+
#
|
173
|
+
# Capabilities are /driver/ capabilities and not IMAP capabilities in
|
174
|
+
# the traditional sense (although there are a few correlations).
|
175
|
+
#
|
176
|
+
# You will generally not work with these directly, outside of passing
|
177
|
+
# them to the EyeMap.connect() call, or if you're writing a
|
178
|
+
# driver.
|
179
|
+
#
|
180
|
+
# All items are symbols, so while not listed here, they start with a colon:
|
181
|
+
#
|
182
|
+
# * delimiter: The folder delimiter that the IMAP server uses.
|
183
|
+
# * folder_class: The class that new folder objects are created from.
|
184
|
+
# * message_class: The class that new message objects are created
|
185
|
+
# from.
|
186
|
+
# * driver_class: Calculated from 'driver', this is the class of
|
187
|
+
# the driver that is being used.
|
188
|
+
# * driver: The 'text' name of the driver, used to locate the
|
189
|
+
# driver.
|
190
|
+
# * user: The username to connect to the IMAP store with.
|
191
|
+
# * password: The password to connect to the IMAP store with.
|
192
|
+
# * host: The host of the IMAP store.
|
193
|
+
# * ssl: Set to true to use SSL to connect to the IMAP
|
194
|
+
# store.
|
195
|
+
# * auth_mech: The authentication mechanism to use.
|
196
|
+
# * verify_ssl: Verify our SSL connection?
|
197
|
+
# * port: The port to use in our IMAP connection.
|
198
|
+
# * cert: The certificate to use (SSL only)
|
199
|
+
#
|
200
|
+
|
201
|
+
def EyeMap.connect(*args)
|
202
|
+
args = args[0]
|
203
|
+
|
204
|
+
if ! args[:driver]
|
205
|
+
args[:driver] = 'auto'
|
206
|
+
end
|
207
|
+
|
208
|
+
# require the appropriate driver from the driver/ directory
|
209
|
+
begin
|
210
|
+
require "eyemap/drivers/#{args[:driver]}"
|
211
|
+
rescue LoadError => e
|
212
|
+
throw EyeMap::Exception::InvalidDriver.new("Driver '#{args[:driver]}' doesn't exist or isn't working properly.")
|
213
|
+
end
|
214
|
+
|
215
|
+
# fetch the driver's class object from the symbol table (Kernel is hte root level)
|
216
|
+
# and store it in the :driver_class argument.
|
217
|
+
args[:driver_class] = EyeMap::Driver.const_get(Inflector.camelize(args[:driver]).to_sym)
|
218
|
+
|
219
|
+
# call, connect, and return the value of the constructor
|
220
|
+
obj = args[:driver_class].new(args)
|
221
|
+
|
222
|
+
obj.connect()
|
223
|
+
return obj
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
def initialize
|
228
|
+
throw EyeMap::Exception::BadCall.new("Use EyeMap.connect() to connect to a driver")
|
229
|
+
end
|
230
|
+
|
231
|
+
end # EyeMap
|
232
|
+
|
233
|
+
EyeMap::Capabilities = Struct.new(:delimiter, :folder_class,
|
234
|
+
:message_class, :driver_class,
|
235
|
+
:driver, :user, :password,
|
236
|
+
:host, :ssl, :auth_mech,
|
237
|
+
:verify_ssl, :port,
|
238
|
+
:cert)
|
@@ -0,0 +1,211 @@
|
|
1
|
+
class EyeMap::Folder
|
2
|
+
|
3
|
+
attr_reader :properties
|
4
|
+
attr_reader :folder_name
|
5
|
+
attr_reader :driver
|
6
|
+
attr_reader :delimiter
|
7
|
+
|
8
|
+
#
|
9
|
+
# Constructor. Inheriting drivers must implement this interface.
|
10
|
+
#
|
11
|
+
|
12
|
+
def initialize(folder_name, driver, delimiter, properties={ })
|
13
|
+
@driver = driver
|
14
|
+
@folder_name = (folder_name and folder_name.length > 0) ? folder_name : "INBOX"
|
15
|
+
@folder_name.freeze
|
16
|
+
@delimiter = delimiter
|
17
|
+
@properties = properties || Hash.new
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Get a specific folder underneath this one.
|
22
|
+
#
|
23
|
+
|
24
|
+
def folder(folder_name)
|
25
|
+
self.class.new(@folder_name + @delimiter + folder_name,
|
26
|
+
@driver,
|
27
|
+
@delimiter)
|
28
|
+
end
|
29
|
+
|
30
|
+
# return the 'short name' of the folder - the name of the folder
|
31
|
+
# without anything "lower" than the delimiter (including the
|
32
|
+
# delimiter itself).
|
33
|
+
|
34
|
+
def short_name
|
35
|
+
return @folder_name.sub(/.*?([^#{@delimiter}]+)$/, '\1')
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Get the list of subfolders for this folder.
|
40
|
+
#
|
41
|
+
|
42
|
+
def subfolders
|
43
|
+
list = @driver.conn.list(@folder_name, "%")
|
44
|
+
|
45
|
+
retval = []
|
46
|
+
|
47
|
+
if list
|
48
|
+
list.each do |f|
|
49
|
+
retval.push(@driver[:folder_class].
|
50
|
+
new(f.name, @driver, f.delim,
|
51
|
+
# deep copy
|
52
|
+
Marshal.load(Marshal.dump(f.attr))))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return retval
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Returns all the folders below this folder in a series of ::Folder objects.
|
61
|
+
#
|
62
|
+
|
63
|
+
def folder_tree
|
64
|
+
skel = { :folder => nil, :children => [] }
|
65
|
+
retval = []
|
66
|
+
subfolders.each do |sub|
|
67
|
+
folder = skel.dup
|
68
|
+
folder[:folder] = sub
|
69
|
+
folder[:children] = folder[:folder].folder_tree
|
70
|
+
retval.push(folder)
|
71
|
+
end
|
72
|
+
|
73
|
+
return retval
|
74
|
+
end
|
75
|
+
|
76
|
+
def total
|
77
|
+
@driver.conn.status(@folder_name, ["MESSAGES"])["MESSAGES"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def recent
|
81
|
+
@driver.conn.status(@folder_name, ["RECENT"])["RECENT"]
|
82
|
+
end
|
83
|
+
|
84
|
+
def unseen
|
85
|
+
@driver.conn.status(@folder_name, ["UNSEEN"])["UNSEEN"]
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_message(message_id)
|
89
|
+
@driver.conn.examine(@folder_name)
|
90
|
+
|
91
|
+
if ! message_id.kind_of? Numeric
|
92
|
+
raise EyeMap::Exception::BadCall.new("Message ID must be numeric")
|
93
|
+
end
|
94
|
+
|
95
|
+
messages = @driver.conn.fetch(message_id, "UID")
|
96
|
+
uid = nil
|
97
|
+
|
98
|
+
uid = messages[0].attr["UID"] if messages[0]
|
99
|
+
|
100
|
+
return @driver[:message_class].new(uid, @folder_name, @driver) if uid
|
101
|
+
|
102
|
+
return nil
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Search through a range of message ids (or an array of them).
|
107
|
+
#
|
108
|
+
# pass 'nil' to search for all messages (the default)
|
109
|
+
#
|
110
|
+
|
111
|
+
def find_messages(message_ids=nil)
|
112
|
+
# this next line of code would be great for AOP
|
113
|
+
@driver.conn.examine(@folder_name)
|
114
|
+
messages = []
|
115
|
+
|
116
|
+
if message_ids.kind_of? Range
|
117
|
+
query = "#{message_ids.first}:#{message_ids.last}"
|
118
|
+
elsif message_ids.kind_of? Array
|
119
|
+
query = message_ids.join(",")
|
120
|
+
elsif ! message_ids
|
121
|
+
query = "ALL"
|
122
|
+
elsif message_ids.kind_of? Numeric
|
123
|
+
return [find_message(message_ids)]
|
124
|
+
else
|
125
|
+
raise EyeMap::Exception::BadCall.new("Invalid query parameters")
|
126
|
+
end
|
127
|
+
|
128
|
+
@driver.conn.uid_search([query]).each do |uid|
|
129
|
+
messages.push(@driver[:message_class].
|
130
|
+
new(uid, @folder_name, @driver))
|
131
|
+
end
|
132
|
+
|
133
|
+
return messages
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# Get a list of valid messages for the current folder, that contain
|
139
|
+
# the number of items per page and reflect the current "page" for
|
140
|
+
# that inbox: page = (num_per_page * 10) - 9 -> (num_per_page *
|
141
|
+
# 10). The reason for this is that IMAP message id's start with 1.
|
142
|
+
#
|
143
|
+
|
144
|
+
def paginate_headers(page, num_per_page=10)
|
145
|
+
@driver.conn.examine(@folder_name)
|
146
|
+
uids = @driver.conn.uid_search(["ALL"])
|
147
|
+
|
148
|
+
# get out now if we don't have any messages.
|
149
|
+
unless uids and uids.length > 0
|
150
|
+
return []
|
151
|
+
end
|
152
|
+
|
153
|
+
# for our query, the high and low mid's, respective to the array.
|
154
|
+
low_mid = (page * num_per_page) - (num_per_page - 1)
|
155
|
+
high_mid = page * num_per_page
|
156
|
+
|
157
|
+
# no messages this high
|
158
|
+
if uids[low_mid].nil?
|
159
|
+
return []
|
160
|
+
end
|
161
|
+
|
162
|
+
if uids[high_mid].nil?
|
163
|
+
high_mid = -1
|
164
|
+
end
|
165
|
+
|
166
|
+
return uids[low_mid..high_mid]
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
#
|
171
|
+
# Permanently deletes all messages marked with the :Deleted flag
|
172
|
+
#
|
173
|
+
|
174
|
+
def expunge
|
175
|
+
@driver.conn.examine(@folder_name)
|
176
|
+
@driver.conn.expunge()
|
177
|
+
end
|
178
|
+
|
179
|
+
#
|
180
|
+
# Creates a new folder underneath this one with the name specified.
|
181
|
+
#
|
182
|
+
# It will prepend the current folder information and the delimiter
|
183
|
+
# to the folder name passed.
|
184
|
+
#
|
185
|
+
|
186
|
+
def create(folder_name)
|
187
|
+
new_folder = @folder_name + @delimiter + folder_name
|
188
|
+
@driver.conn.create(new_folder)
|
189
|
+
self.class.new(new_folder, @driver, @delimiter)
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Delete this folder.
|
194
|
+
#
|
195
|
+
|
196
|
+
def delete
|
197
|
+
@driver.conn.delete(@folder_name)
|
198
|
+
end
|
199
|
+
|
200
|
+
#
|
201
|
+
# Add a new message to this folder.
|
202
|
+
#
|
203
|
+
# Our message in this case is just a block of text. This does not use a message object.
|
204
|
+
#
|
205
|
+
|
206
|
+
def add_message(message_text)
|
207
|
+
@driver.conn.append(@folder_name, message_text.gsub(/[^\r]\n/, "\r\n"),
|
208
|
+
[], Time.now)
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class EyeMap::Message
|
2
|
+
|
3
|
+
def initialize(uid, folder_name, driver)
|
4
|
+
@uid = uid
|
5
|
+
@folder_name = folder_name
|
6
|
+
@driver = driver
|
7
|
+
end
|
8
|
+
|
9
|
+
def envelope
|
10
|
+
@driver.conn.uid_fetch(@uid, "ENVELOPE")[0].attr["ENVELOPE"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def move(folder)
|
14
|
+
copy(folder)
|
15
|
+
delete
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy(folder)
|
19
|
+
@driver.conn.uid_copy(@uid, folder.folder_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Fetches headers and sanitizes various fields from the IMAP message.
|
24
|
+
# Takes an array of header names.
|
25
|
+
#
|
26
|
+
|
27
|
+
def headers(headers)
|
28
|
+
@driver.conn.examine(@folder_name)
|
29
|
+
|
30
|
+
fetch_command = "BODY.PEEK[HEADER.FIELDS ("
|
31
|
+
|
32
|
+
fetch_fields = ""
|
33
|
+
headers.each do |h|
|
34
|
+
fetch_fields << h << " "
|
35
|
+
end
|
36
|
+
|
37
|
+
fetch_command << fetch_fields.strip << ")]"
|
38
|
+
|
39
|
+
return_headers = Hash.new
|
40
|
+
|
41
|
+
# send the command - Net::IMAP returns the value as
|
42
|
+
# BODY[HEADER.FIELDS instead of
|
43
|
+
# BODY.PEEK[HEADER.FIELDS, so we compensate for that.
|
44
|
+
body = @driver.conn.uid_fetch(@uid, fetch_command)[0].
|
45
|
+
attr["BODY[HEADER.FIELDS (" + fetch_fields.upcase.strip + ")]"]
|
46
|
+
|
47
|
+
# pull out the fields we just nabbed
|
48
|
+
|
49
|
+
headers.each do |h|
|
50
|
+
r = Regexp.new %r!(?:^|\r\n)(#{h}:.+?)\r\n!i
|
51
|
+
header = nil
|
52
|
+
while m = r.match(body)
|
53
|
+
match = m[1]
|
54
|
+
match.sub! /^#{h}:\s/, ""
|
55
|
+
|
56
|
+
if header
|
57
|
+
header = [header] unless header.kind_of? Array
|
58
|
+
header.push(match)
|
59
|
+
else
|
60
|
+
header = match
|
61
|
+
end
|
62
|
+
|
63
|
+
body.sub! /(?:^|\r\n)(#{h}:.+?)\r\n/i, ""
|
64
|
+
end
|
65
|
+
|
66
|
+
return_headers[h] = header
|
67
|
+
end
|
68
|
+
|
69
|
+
return return_headers
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Delete a message from the mail store.
|
75
|
+
#
|
76
|
+
# Unfortunately, as soon as you close this mailbox or select
|
77
|
+
# another, the messages that were marked deleted will be expunged
|
78
|
+
# due to the nature of the IMAP protocol.
|
79
|
+
#
|
80
|
+
# Instead of using this call at all, it would be wiser to move your
|
81
|
+
# messages to delete to a separate folder and then delete them there
|
82
|
+
# when you are ready.
|
83
|
+
#
|
84
|
+
|
85
|
+
def delete()
|
86
|
+
@driver.conn.select(@folder_name)
|
87
|
+
@driver.conn.uid_store(@uid, "+FLAGS", [:Deleted])
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# This does the opposite of delete()
|
92
|
+
#
|
93
|
+
|
94
|
+
def undelete()
|
95
|
+
@driver.conn.select(@folder_name)
|
96
|
+
@driver.conn.uid_store(@uid, "-FLAGS", [:Deleted])
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/test/activeimap.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'eyemap'
|
3
|
+
|
4
|
+
Dir['lib/eyemap/drivers/*.rb'].each do |x|
|
5
|
+
load x
|
6
|
+
end
|
7
|
+
|
8
|
+
$USER = ENV['EYEMAP_USER']
|
9
|
+
$PASS = ENV['EYEMAP_PASS']
|
10
|
+
$HOST = ENV['EYEMAP_HOST']
|
11
|
+
$DOVECOT_PORT = ENV['EYEMAP_DOVECOT_PORT']
|
12
|
+
$COURIER_PORT = ENV['EYEMAP_COURIER_PORT']
|
13
|
+
|
14
|
+
#
|
15
|
+
# Most of the tests just work against dovecot and courier right now.
|
16
|
+
#
|
17
|
+
# Ideally, we should have tests which work against each connection in their
|
18
|
+
# own test file with a driver or something that communicates with a inner series
|
19
|
+
# of tests. These tests would assert general functionality while maintaining their
|
20
|
+
# specific-ness.
|
21
|
+
#
|
22
|
+
|
23
|
+
module GenericTestsMain
|
24
|
+
|
25
|
+
def test_folder
|
26
|
+
assert_kind_of(EyeMap::Folder, @conn.folder)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_capabilities
|
30
|
+
assert_kind_of(Class, @conn[:driver_class])
|
31
|
+
assert_kind_of(Class, @conn[:message_class])
|
32
|
+
assert_kind_of(Class, @conn[:folder_class])
|
33
|
+
assert_kind_of(String, @conn[:driver])
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_connections
|
37
|
+
assert(@conn.connected?)
|
38
|
+
assert_nothing_raised() { @conn.disconnect }
|
39
|
+
assert(!@conn.connected?)
|
40
|
+
assert_nothing_raised() { @conn.connect }
|
41
|
+
assert(@conn.connected?)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
module GenericTestsFolder
|
47
|
+
|
48
|
+
def test_subfolders_and_create_and_delete
|
49
|
+
if @conn[:driver_class] == EyeMap::Driver::Dovecot
|
50
|
+
assert_nothing_raised() { @conn.folder.subfolders }
|
51
|
+
assert_equal([], @conn.folder.subfolders)
|
52
|
+
assert_raise(EyeMap::Exception::DriverIncapable) do
|
53
|
+
@conn.folder.folder("monkey")
|
54
|
+
end
|
55
|
+
assert_raise(EyeMap::Exception::DriverIncapable) do
|
56
|
+
@conn.folder.create
|
57
|
+
end
|
58
|
+
else
|
59
|
+
folders = nil
|
60
|
+
assert_nothing_raised() { folders = @conn.folder.subfolders }
|
61
|
+
|
62
|
+
if folders.collect { |f| f.short_name }.include? "monkey"
|
63
|
+
assert_nothing_raised() { @conn.folder.folder("monkey").delete }
|
64
|
+
else
|
65
|
+
assert_raise(Net::IMAP::NoResponseError) { @conn.folder.folder("monkey").delete }
|
66
|
+
end
|
67
|
+
|
68
|
+
assert_kind_of(EyeMap::Folder, @conn.folder.create("monkey"))
|
69
|
+
assert_nothing_raised() { @conn.folder.folder("monkey").delete }
|
70
|
+
end
|
71
|
+
end # test_subfolders_and_create_and_delete
|
72
|
+
|
73
|
+
def test_short_name
|
74
|
+
assert_equal(@conn.folder("INBOX").folder_name, @conn.folder("INBOX").short_name)
|
75
|
+
|
76
|
+
unless @conn[:driver_class] == EyeMap::Driver::Dovecot
|
77
|
+
assert_nothing_raised() { @conn.folder("INBOX").create("Monkey") }
|
78
|
+
assert_equal("Monkey", @conn.folder("INBOX").folder("Monkey").short_name)
|
79
|
+
assert_nothing_raised() { @conn.folder("INBOX").folder("Monkey").delete }
|
80
|
+
end
|
81
|
+
end # test_short_name
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
module GenericTestsMessage
|
86
|
+
def test_message
|
87
|
+
message = ""
|
88
|
+
assert_nothing_raised() do
|
89
|
+
f = File.open("test/test_message")
|
90
|
+
tmp_irs = $/
|
91
|
+
$/ = nil
|
92
|
+
message << f.readline
|
93
|
+
$/ = tmp_irs
|
94
|
+
f.close
|
95
|
+
end
|
96
|
+
|
97
|
+
folder = nil
|
98
|
+
new_message = nil
|
99
|
+
|
100
|
+
assert_nothing_raised() do
|
101
|
+
if @conn[:driver_class] == EyeMap::Driver::Dovecot
|
102
|
+
folder = @conn.create("EyeMap")
|
103
|
+
else
|
104
|
+
folder = @conn.folder.create("EyeMap")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_nothing_raised() do
|
109
|
+
@conn.folder.add_message(message.gsub(/(?:[^\r])\n/, "\r\n"))
|
110
|
+
# type change: String -> EyeMap::Message
|
111
|
+
message = @conn.folder.find_message(@conn.folder.total)
|
112
|
+
message.copy(folder)
|
113
|
+
new_message = @conn.folder(folder.folder_name).find_message(@conn.folder(folder.folder_name).total)
|
114
|
+
end
|
115
|
+
|
116
|
+
assert_equal(new_message.headers(%w(Subject)), message.headers(%w(Subject)))
|
117
|
+
|
118
|
+
assert_nothing_raised() do
|
119
|
+
new_message.delete
|
120
|
+
message.move(folder)
|
121
|
+
new_message = @conn.folder(folder.folder_name).find_message(@conn.folder(folder.folder_name).total)
|
122
|
+
end
|
123
|
+
|
124
|
+
assert_equal(new_message.headers(%w(Subject)), message.headers(%w(Subject)))
|
125
|
+
|
126
|
+
assert_nothing_raised() do
|
127
|
+
new_message.delete
|
128
|
+
folder.delete
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class TestDovecotEyeMap < Test::Unit::TestCase
|
134
|
+
|
135
|
+
def setup
|
136
|
+
@conn = EyeMap.connect(:driver => 'dovecot',
|
137
|
+
:user => $USER,
|
138
|
+
:password => $PASS,
|
139
|
+
:port => $DOVECOT_PORT,
|
140
|
+
:host => $HOST)
|
141
|
+
end
|
142
|
+
|
143
|
+
include GenericTestsMain
|
144
|
+
include GenericTestsFolder
|
145
|
+
include GenericTestsMessage
|
146
|
+
end
|
147
|
+
|
148
|
+
class TestCourierEyeMap < Test::Unit::TestCase
|
149
|
+
def setup
|
150
|
+
@conn = EyeMap.connect(:driver => 'courier',
|
151
|
+
:port => $COURIER_PORT,
|
152
|
+
:user => $USER,
|
153
|
+
:password => $PASS,
|
154
|
+
:host => $HOST)
|
155
|
+
end
|
156
|
+
|
157
|
+
include GenericTestsMain
|
158
|
+
include GenericTestsFolder
|
159
|
+
include GenericTestsMessage
|
160
|
+
end
|
data/test/test_message
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.2
|
3
|
+
specification_version: 1
|
4
|
+
name: eyemap
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.8.0
|
7
|
+
date: 2007-03-26 00:00:00 -07:00
|
8
|
+
summary: Provides a Driver Architecture and Framework for talking to IMAP servers
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: erik@hollensbe.org
|
12
|
+
homepage:
|
13
|
+
rubyforge_project: eyemap
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Erik Hollensbe
|
31
|
+
files:
|
32
|
+
- lib/eyemap.rb
|
33
|
+
- lib/eyemap/exception.rb
|
34
|
+
- lib/eyemap/eyemap.rb
|
35
|
+
- lib/eyemap/folder.rb
|
36
|
+
- lib/eyemap/message.rb
|
37
|
+
- lib/eyemap/search.rb
|
38
|
+
- lib/eyemap/drivers/auto.rb
|
39
|
+
- lib/eyemap/drivers/courier.rb
|
40
|
+
- lib/eyemap/drivers/courier_folder.rb
|
41
|
+
- lib/eyemap/drivers/dovecot.rb
|
42
|
+
- lib/eyemap/drivers/dovecot_folder.rb
|
43
|
+
- test/activeimap.rb
|
44
|
+
- test/test_message
|
45
|
+
test_files: []
|
46
|
+
|
47
|
+
rdoc_options: []
|
48
|
+
|
49
|
+
extra_rdoc_files: []
|
50
|
+
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
dependencies:
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: activesupport
|
60
|
+
version_requirement:
|
61
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">"
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 0.0.0
|
66
|
+
version:
|