eyemap 0.8.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.
- 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:
|