lambder-rexchange 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +52 -0
- data/MIT-LICENSE +22 -0
- data/RAKEFILE +75 -0
- data/README +86 -0
- data/lib/r_exchange.rb +2 -0
- data/lib/rexchange.rb +33 -0
- data/lib/rexchange/appointment.rb +38 -0
- data/lib/rexchange/contact.rb +29 -0
- data/lib/rexchange/credentials.rb +38 -0
- data/lib/rexchange/dav_get_request.rb +28 -0
- data/lib/rexchange/dav_move_request.rb +30 -0
- data/lib/rexchange/dav_search_request.rb +8 -0
- data/lib/rexchange/exchange_request.rb +86 -0
- data/lib/rexchange/folder.rb +102 -0
- data/lib/rexchange/generic_item.rb +124 -0
- data/lib/rexchange/message.rb +88 -0
- data/lib/rexchange/note.rb +16 -0
- data/lib/rexchange/session.rb +28 -0
- data/lib/rexchange/task.rb +26 -0
- data/test/functional.rb +23 -0
- metadata +80 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
-- 0.3.4:
|
2
|
+
* Updated the README so it jives with the current version
|
3
|
+
* Removed Folder::join since it's only used by Folder#to_s, and just used a simpler
|
4
|
+
solution in Folder#to_s instead
|
5
|
+
|
6
|
+
-- 0.3.3:
|
7
|
+
* Folder objects now default to using the Message class for enumeration
|
8
|
+
if no class is associated with the content_type of the Folder.
|
9
|
+
|
10
|
+
-- 0.3.2:
|
11
|
+
* Quick hack to make Message#move_to work
|
12
|
+
|
13
|
+
-- 0.3.1:
|
14
|
+
* Disabled a warning about overwriting GenericItem::CONTENT_CLASS
|
15
|
+
* Added a "rescue" for Time::parse failures that reverts to the original element text
|
16
|
+
|
17
|
+
-- 0.3.0:
|
18
|
+
* Added support for Appointments
|
19
|
+
* Options hash for Session creation is gone. The new form is: "RExchange::Session.new(url, username, password)"
|
20
|
+
* Removed GenericItem#[], GenericItem#method_missing (attributes are explicit now)
|
21
|
+
* Removed Folder#get_messages, #get_contacts and #messages_in. Folders are now Enumerables using Enumerable#entries, #each, etc
|
22
|
+
to retrieve entries according to the folder's content-class. So a 'mailfolder' will retrieve Message instances,
|
23
|
+
a 'calendarfolder' will retrieve Appointment instances, and so on.
|
24
|
+
|
25
|
+
-- 0.2.0:
|
26
|
+
* Fixed a bug in Message#move_to when passing folders
|
27
|
+
* Added support for Contact browsing
|
28
|
+
* Added GenericItem to abstract Contacts and Messages
|
29
|
+
* Renamed Folder#messages to Folder#get_messages to avoid naming collisions
|
30
|
+
* Modified attributes in GenericItem that end with "date" to be Time::parse'd
|
31
|
+
* Added "r_exchange.rb" for Rails auto-require compatibility
|
32
|
+
* Now calling String#normalize on attribute names instead of just String#tr('-', '_')
|
33
|
+
|
34
|
+
-- 0.1.4:
|
35
|
+
* Moved Folder::normalize_folder_name to String#normalize, and added support for MixedCase sources.
|
36
|
+
* Fixed a bug where the normalized folder name was being used for the Folder#to_s result.
|
37
|
+
* Moved the meat of Folder#messages to Message::find
|
38
|
+
* Added Message#to_s
|
39
|
+
* Modified Folder#get_folders to return a hash instead of an array, providing both the original, and normalized names.
|
40
|
+
|
41
|
+
-- 0.1.3: Bugfixes
|
42
|
+
* Fixed several nasty bugs.
|
43
|
+
|
44
|
+
-- 0.1.2: Minor documentation/cleanup
|
45
|
+
* Removed a double-dash in the README that was apparently causing some viewing errors
|
46
|
+
* Changed Session initialize to a ternary condition
|
47
|
+
* Added RExchange::Message#has_attachments?
|
48
|
+
* Added documentation for most methods in RExchange::Message and RExchange::Folder
|
49
|
+
|
50
|
+
-- 0.1.1
|
51
|
+
* Fleshed out the README
|
52
|
+
* Copied the RAKEFILE from the Rools project
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2006 Samuel Smoot
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/RAKEFILE
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rake/rdoctask'
|
7
|
+
require 'rake/gempackagetask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'pscp'
|
10
|
+
|
11
|
+
PACKAGE_VERSION = '0.3.4'
|
12
|
+
|
13
|
+
PACKAGE_FILES = FileList[
|
14
|
+
'README',
|
15
|
+
'CHANGELOG',
|
16
|
+
'RAKEFILE',
|
17
|
+
'MIT-LICENSE',
|
18
|
+
'lib/**/*.rb',
|
19
|
+
'test/*.rb'
|
20
|
+
].to_a
|
21
|
+
|
22
|
+
PROJECT = 'rexchange'
|
23
|
+
|
24
|
+
ENV['RUBYFORGE_USER'] = "ssmoot@rubyforge.org"
|
25
|
+
ENV['RUBYFORGE_PROJECT'] = "/var/www/gforge-projects/#{PROJECT}"
|
26
|
+
|
27
|
+
task :default => [:rdoc]
|
28
|
+
|
29
|
+
desc 'Generate Documentation'
|
30
|
+
rd = Rake::RDocTask.new do |rdoc|
|
31
|
+
rdoc.rdoc_dir = 'doc'
|
32
|
+
rdoc.title = "RExchange -- A simple wrapper around Microsoft Exchange Server's WebDAV API"
|
33
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
34
|
+
rdoc.rdoc_files.include(PACKAGE_FILES)
|
35
|
+
end
|
36
|
+
|
37
|
+
gem_spec = Gem::Specification.new do |s|
|
38
|
+
s.platform = Gem::Platform::RUBY
|
39
|
+
s.name = PROJECT
|
40
|
+
s.summary = "A simple wrapper around Microsoft Exchange Server's WebDAV API"
|
41
|
+
s.description = "Connect, browse, and iterate through folders and messages on an Exchange Server"
|
42
|
+
s.version = PACKAGE_VERSION
|
43
|
+
|
44
|
+
s.authors = 'Sam Smoot', 'Scott Bauer'
|
45
|
+
s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com'
|
46
|
+
s.rubyforge_project = PROJECT
|
47
|
+
s.homepage = 'http://substantiality.net'
|
48
|
+
|
49
|
+
s.files = PACKAGE_FILES
|
50
|
+
|
51
|
+
s.require_path = 'lib'
|
52
|
+
s.requirements << 'none'
|
53
|
+
s.autorequire = 'rexchange'
|
54
|
+
|
55
|
+
s.has_rdoc = true
|
56
|
+
s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
57
|
+
s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
|
58
|
+
end
|
59
|
+
|
60
|
+
Rake::GemPackageTask.new(gem_spec) do |p|
|
61
|
+
p.gem_spec = gem_spec
|
62
|
+
p.need_tar = true
|
63
|
+
p.need_zip = true
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Publish RDOC to RubyForge"
|
67
|
+
task :rubyforge => [:rdoc, :gem] do
|
68
|
+
Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'doc').upload
|
69
|
+
end
|
70
|
+
|
71
|
+
Rake::TestTask.new do |t|
|
72
|
+
t.libs << "test"
|
73
|
+
t.test_files = FileList['test/*.rb']
|
74
|
+
t.verbose = true
|
75
|
+
end
|
data/README
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
= RExchange
|
2
|
+
|
3
|
+
RExchange is a pure ruby wrapper for the Microsoft Exchange Server WebDAV API
|
4
|
+
|
5
|
+
== Things you should know
|
6
|
+
|
7
|
+
* Requires Ruby 1.8.4 (or later, for the extended WebDAV support in the net/http library)
|
8
|
+
* RExchange is cross-platform compatible, being written in pure Ruby
|
9
|
+
* Kiwi fruits are packed with vitamins
|
10
|
+
|
11
|
+
== Why should you use RExchange
|
12
|
+
|
13
|
+
* It makes interacting with Exchange simple
|
14
|
+
* It was written for a real application, and does real work reliably day in and day out
|
15
|
+
|
16
|
+
== Example
|
17
|
+
|
18
|
+
# We pass our uri (pointing directly to a mailbox), a username, and a password to RExchange::open
|
19
|
+
# to create a RExchange::Session. Notice that we escape the "\" in our username.
|
20
|
+
RExchange::open('https://example.com/exchange/admin/', 'mydomain\\admin', 'secret') do |mailbox|
|
21
|
+
|
22
|
+
# The block parameter ("mailbox" in this case) is actually the Session itself.
|
23
|
+
# You can refer to folders by chaining them as method calls. "inbox" in this case
|
24
|
+
# isn't a defined method for Session, but part of the DSL to refer to folder names.
|
25
|
+
# Each folder name returns a RExchange::Folder. The folder is Enumerable, allowing
|
26
|
+
# iteration over the items in the folder, depending on the DAV:content-class of
|
27
|
+
# the folder.
|
28
|
+
mailbox.inbox.each do |message|
|
29
|
+
|
30
|
+
# The "message" block parameter is a RExchange::Message object. You have access to
|
31
|
+
# several attributes of the message, including: href, from, to, message_id, date,
|
32
|
+
# importance, has_attachment? and body.
|
33
|
+
p message.subject
|
34
|
+
|
35
|
+
# The RExchange::Message#move_to method moves the message to another folder, in this
|
36
|
+
# case, an "archive" folder off of the inbox.
|
37
|
+
message.move_to mailbox.inbox.archive
|
38
|
+
end
|
39
|
+
|
40
|
+
# Folder names are "normalized", replacing dashes and spaces with underscores,
|
41
|
+
# squeezing out multiple underscores in a row, and downcasing the whole thing.
|
42
|
+
# So a folder name such as "My Very-long Folder Name" would look like:
|
43
|
+
mailbox.my_very_long_folder_name
|
44
|
+
|
45
|
+
# The Enumerable mixin gives us an Folder#entries method in case we just want to
|
46
|
+
# return an array.
|
47
|
+
messages = mailbox.inbox.entries
|
48
|
+
|
49
|
+
# Other methods, like Enumerable#map and Enumerable#sort_by let us manipulate
|
50
|
+
# the Folder in fun ways:
|
51
|
+
puts mailbox.contacts.sort_by { |contact| contact.first_name }.map do |contact|
|
52
|
+
<<-CONTACT
|
53
|
+
Name: #{contact.first_name} #{contact.last_name}
|
54
|
+
Email: #{contact.email}
|
55
|
+
Phone: #{contact.phone}
|
56
|
+
CONTACT
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
# We can access the Global Address Book by accessing the "/public" folder:
|
62
|
+
RExchange::open('https://example.com/public', 'mydomain\\somebody', 'secret') do |mailbox|
|
63
|
+
|
64
|
+
puts mailbox.company_contacts.each do |contact|
|
65
|
+
<<-CONTACT
|
66
|
+
Company: #{contact.company}
|
67
|
+
Name: #{contact.first_name} #{contact.last_name}
|
68
|
+
Email: #{contact.email}
|
69
|
+
Phone: #{contact.phone}
|
70
|
+
CONTACT
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
== Caveats
|
76
|
+
|
77
|
+
There are several features missing (simply because we didn't need them yet). Among them:
|
78
|
+
|
79
|
+
* The ability to delete messages or folders
|
80
|
+
* The ability to create folders
|
81
|
+
* There's no mechanism for sending new messages, or replying or forwarding existing ones
|
82
|
+
* There are a lot more message attributes we're not retrieving since they weren't useful to us, but they might be to you
|
83
|
+
* Exporting an email or entire folder tree to offline storage
|
84
|
+
* And much much more!
|
85
|
+
|
86
|
+
If you'd like to see any of these features, or have some ideas of your own you'd like to see implemented don't hesitate to let us know, and if it strikes our fancy maybe you'll get some free programming!
|
data/lib/r_exchange.rb
ADDED
data/lib/rexchange.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rexchange/session'
|
2
|
+
require 'rexchange/r_exception'
|
3
|
+
|
4
|
+
class String
|
5
|
+
def ends_with?(partial)
|
6
|
+
self[self.size - partial.size..self.size] == partial
|
7
|
+
end
|
8
|
+
|
9
|
+
def ensure_ends_with(partial)
|
10
|
+
self.ends_with?(partial) ? self : self + partial
|
11
|
+
end
|
12
|
+
|
13
|
+
def normalize
|
14
|
+
self.split(/(?=[A-Z][a-z]*)/).join('_').tr('- ', '_').squeeze('_').downcase
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module RExchange
|
19
|
+
# Use STDOUT or another stream if you'd like to capture the HTTP debug output
|
20
|
+
DEBUG_STREAM = $log
|
21
|
+
|
22
|
+
PR_HTTPMAIL_READ = 'read'
|
23
|
+
NS_HTTPMAIL = 'urn:schemas:httpmail'
|
24
|
+
|
25
|
+
# A shortcut to RExchange::Session#new
|
26
|
+
def self.open(dav_uri, owa_uri, username = nil, password = nil)
|
27
|
+
session = RExchange::Session.new(dav_uri, owa_uri, username, password)
|
28
|
+
|
29
|
+
yield session if block_given?
|
30
|
+
return session
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rexchange/generic_item'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
class Appointment < GenericItem
|
5
|
+
|
6
|
+
set_folder_type 'calendar'
|
7
|
+
|
8
|
+
attribute_mappings :all_day_event => 'urn:schemas:calendar:alldayevent',
|
9
|
+
:busy_status => 'urn:schemas:calendar:busystatus',
|
10
|
+
:contact => 'urn:schemas:calendar:contact',
|
11
|
+
:contact_url => 'urn:schemas:calendar:contacturl',
|
12
|
+
:created_on => 'urn:schemas:calendar:created',
|
13
|
+
:description_url => 'urn:schemas:calendar:descriptionurl',
|
14
|
+
:end_at => 'urn:schemas:calendar:dtend',
|
15
|
+
:created_at => 'urn:schemas:calendar:dtstamp',
|
16
|
+
:start_at => 'urn:schemas:calendar:dtstart',
|
17
|
+
:duration => 'urn:schemas:calendar:duration',
|
18
|
+
:expires_on => 'urn:schemas:calendar:exdate',
|
19
|
+
:expiry_rule => 'urn:schemas:calendar:exrule',
|
20
|
+
:has_attachment? => 'urn:schemas:httpmail:hasattachment',
|
21
|
+
:html => 'urn:schemas:httpmail:htmldescription',
|
22
|
+
:modified_on => 'urn:schemas:calendar:lastmodified',
|
23
|
+
:location => 'urn:schemas:calendar:location',
|
24
|
+
:location_url => 'urn:schemas:calendar:locationurl',
|
25
|
+
:meeting_status => 'urn:schemas:calendar:meetingstatus',
|
26
|
+
:normalized_subject => 'urn:schemas:httpmail:normalizedsubject',
|
27
|
+
:priority => 'urn:schemas:httpmail:priority',
|
28
|
+
:recurres_on => 'urn:schemas:calendar:rdate',
|
29
|
+
:reminder_offset => 'urn:schemas:calendar:reminderoffset',
|
30
|
+
:reply_time => 'urn:schemas:calendar:replytime',
|
31
|
+
:sequence => 'urn:schemas:calendar:sequence',
|
32
|
+
:subject => 'urn:schemas:httpmail:subject',
|
33
|
+
:body => 'urn:schemas:httpmail:textdescription',
|
34
|
+
:timezone => 'urn:schemas:calendar:timezone',
|
35
|
+
:uid => 'urn:schemas:calendar:uid'
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rexchange/generic_item'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
class Contact < GenericItem
|
5
|
+
|
6
|
+
set_folder_type 'contact'
|
7
|
+
set_content_class 'person'
|
8
|
+
|
9
|
+
attribute_mappings :first_name => 'urn:schemas:contacts:givenName',
|
10
|
+
:middle_name => 'urn:schemas:contacts:middlename',
|
11
|
+
:last_name => 'urn:schemas:contacts:sn',
|
12
|
+
:title => 'urn:schemas:contacts:title',
|
13
|
+
:created_at => 'DAV:creationdate',
|
14
|
+
:address => 'urn:schemas:contacts:mailingstreet',
|
15
|
+
:city => 'urn:schemas:contacts:mailingcity',
|
16
|
+
:state => 'urn:schemas:contacts:st',
|
17
|
+
:zip_code => 'urn:schemas:contacts:mailingpostalcode',
|
18
|
+
:country => 'urn:schemas:contacts:co',
|
19
|
+
:phone => 'urn:schemas:contacts:homePhone',
|
20
|
+
:business_phone => 'urn:schemas:contacts:telephoneNumber',
|
21
|
+
:fax => 'urn:schemas:contacts:facsimiletelephonenumber',
|
22
|
+
:mobile => 'urn:schemas:contacts:mobile',
|
23
|
+
:email => 'urn:schemas:contacts:email1',
|
24
|
+
:website => 'urn:schemas:contacts:businesshomepage',
|
25
|
+
:company => 'urn:schemas:contacts:o',
|
26
|
+
:notes => 'urn:schemas:httpmail:textdescription'
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
|
5
|
+
# Credentials are passed around between Folders to emulate a stateful
|
6
|
+
# connection with the RExchange::Session
|
7
|
+
class Credentials
|
8
|
+
attr_reader :user, :password, :dav_uri, :owa_uri
|
9
|
+
attr_accessor :auth_cookie
|
10
|
+
|
11
|
+
# You must pass a dav_uri, owa_uri, a username and a password
|
12
|
+
def initialize(dav_uri = nil, owa_uri = nil, username = nil, password = nil)
|
13
|
+
owa_uri = nil if owa_uri && owa_uri.blank?
|
14
|
+
@dav_uri = URI.parse(dav_uri) if dav_uri
|
15
|
+
@owa_uri = URI.parse(owa_uri) if owa_uri
|
16
|
+
@dav_use_ssl = @dav_uri.scheme ? (@dav_uri.scheme.downcase == 'https') : false
|
17
|
+
@user = username || (@dav_uri && @dav_uri.userinfo ? @dav_uri.userinfo.split(':')[0] : nil) || (@owa_uri && @owa_uri.userinfo ? @owa_uri.userinfo.split(':')[0] : nil)
|
18
|
+
@password = password || (@dav_uri && @dav_uri.userinfo ? @dav_uri.userinfo.split(':')[1] : nil) || (@owa_uri && @owa_uri.userinfo ? @owa_uri.userinfo.split(':')[1] : nil)
|
19
|
+
@dav_port = (@dav_uri.port || @dav_uri.default_port) if @dav_uri
|
20
|
+
@owa_port = (@owa_uri.port || @owa_uri.default_port) if @owa_uri
|
21
|
+
if block_given?
|
22
|
+
yield self
|
23
|
+
else
|
24
|
+
return self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def dav_use_ssl?
|
29
|
+
@dav_use_ssl
|
30
|
+
end
|
31
|
+
|
32
|
+
def is_owa?
|
33
|
+
@owa_uri && @owa_uri.scheme && @owa_uri.host
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used for arbitrary SEARCH requests
|
5
|
+
class DavGetRequest < ExchangeRequest
|
6
|
+
METHOD = 'GET'
|
7
|
+
REQUEST_HAS_BODY = false
|
8
|
+
RESPONSE_HAS_BODY = true
|
9
|
+
|
10
|
+
def self.execute(credentials, url, &b)
|
11
|
+
begin
|
12
|
+
options = {
|
13
|
+
:path => url,
|
14
|
+
:headers => {
|
15
|
+
'Translate' => 'f' # Microsoft IIS 5.0 "Translate: f" Source Disclosure Vulnerability (http://www.securityfocus.com/bid/1578)
|
16
|
+
}
|
17
|
+
}
|
18
|
+
response = super credentials, options
|
19
|
+
yield response if b
|
20
|
+
response
|
21
|
+
rescue RException => e
|
22
|
+
raise e
|
23
|
+
rescue Exception => e
|
24
|
+
raise RException.new(options[:request], response, e)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used to move entities to different locations in the accessable mailbox.
|
5
|
+
class DavMoveRequest < ExchangeRequest
|
6
|
+
METHOD = 'MOVE'
|
7
|
+
REQUEST_HAS_BODY = false
|
8
|
+
RESPONSE_HAS_BODY = false
|
9
|
+
|
10
|
+
def self.execute(credentials, source, destination, &b)
|
11
|
+
begin
|
12
|
+
options = {
|
13
|
+
:headers => {
|
14
|
+
'Destination' => destination
|
15
|
+
},
|
16
|
+
:path => source
|
17
|
+
}
|
18
|
+
|
19
|
+
response = super credentials, options
|
20
|
+
yield response if b
|
21
|
+
response
|
22
|
+
rescue RException => e
|
23
|
+
raise e
|
24
|
+
rescue Exception => e
|
25
|
+
raise RException.new(options[:request], response, e)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
|
5
|
+
# Exchange Server's WebDAV interface is non-standard, so
|
6
|
+
# we create this simple wrapper to extend the 'net/http'
|
7
|
+
# library and add the request methods we need.
|
8
|
+
class ExchangeRequest < Net::HTTPRequest
|
9
|
+
REQUEST_HAS_BODY = true
|
10
|
+
RESPONSE_HAS_BODY = true
|
11
|
+
|
12
|
+
def self.authenticate(credentials)
|
13
|
+
owa_uri = credentials.owa_uri
|
14
|
+
if owa_uri
|
15
|
+
http = Net::HTTP.new(owa_uri.host, owa_uri.port)
|
16
|
+
http.set_debug_output(RExchange::DEBUG_STREAM) if RExchange::DEBUG_STREAM
|
17
|
+
req = Net::HTTP::Post.new(owa_uri.path)
|
18
|
+
destination = owa_uri.scheme+"://"+owa_uri.host+(owa_uri.port ? ':'+owa_uri.port.to_s : '')
|
19
|
+
req.body = "destination=#{destination}&username=#{credentials.user}&password=#{credentials.password}"
|
20
|
+
http.use_ssl = owa_uri.scheme ? (owa_uri.scheme.downcase == 'https') : false
|
21
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
22
|
+
res = http.request(req)
|
23
|
+
credentials.auth_cookie = res.header["set-cookie"].split(',').map(&:strip).map{|c| c.split(';')[0]}.reverse.join('; ') if res.header["set-cookie"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.exchange_request(credentials, options = {})
|
28
|
+
http = Net::HTTP.new(credentials.dav_uri.host, credentials.dav_uri.port)
|
29
|
+
http.set_debug_output(RExchange::DEBUG_STREAM) if RExchange::DEBUG_STREAM
|
30
|
+
request_path = options[:path] || credentials.dav_uri.path
|
31
|
+
req = self.new(request_path)
|
32
|
+
options[:request] = req
|
33
|
+
req.basic_auth credentials.user, credentials.password #if !credentials.is_owa?
|
34
|
+
req.content_type = 'text/xml'
|
35
|
+
req.add_field 'host', credentials.dav_uri.host
|
36
|
+
|
37
|
+
if options[:headers]
|
38
|
+
options[:headers].each_pair do |k, v|
|
39
|
+
req.add_field k, v
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
req.body = options[:body] if REQUEST_HAS_BODY
|
44
|
+
http.use_ssl = credentials.dav_use_ssl?
|
45
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
46
|
+
response = http.request(req) if RESPONSE_HAS_BODY
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.execute(credentials, options = {}, &b)
|
51
|
+
begin
|
52
|
+
headers = options[:headers] ||= {}
|
53
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
54
|
+
response = self.exchange_request(credentials, options)
|
55
|
+
case response
|
56
|
+
when Net::HTTPClientError then
|
57
|
+
self.authenticate(credentials)
|
58
|
+
#repeat exchange_request after authentication with the auth-cookies
|
59
|
+
headers = options[:headers] ||= {}
|
60
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
61
|
+
response = self.exchange_request(credentials, options)
|
62
|
+
end
|
63
|
+
|
64
|
+
raise 'NOT 2xx NOR 3xx: ' + response.inspect.to_s unless (Net::HTTPSuccess === response || Net::HTTPRedirection === response)
|
65
|
+
|
66
|
+
yield response if b
|
67
|
+
response
|
68
|
+
rescue RException => e
|
69
|
+
raise e
|
70
|
+
rescue Exception => e
|
71
|
+
#puts e.backtrace.map{|e| e.to_s}.join("\n")
|
72
|
+
raise RException.new(options[:request], response, e)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
# You can not instantiate an ExchangeRequest externally.
|
79
|
+
def initialize(*args)
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'net/https'
|
3
|
+
require 'rexchange/dav_search_request'
|
4
|
+
require 'rexchange/message'
|
5
|
+
require 'rexchange/contact'
|
6
|
+
require 'rexchange/appointment'
|
7
|
+
require 'rexchange/note'
|
8
|
+
require 'rexchange/task'
|
9
|
+
require 'rexchange/message_href'
|
10
|
+
require 'uri'
|
11
|
+
|
12
|
+
module RExchange
|
13
|
+
|
14
|
+
class FolderNotFoundError < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class Folder
|
18
|
+
include REXML
|
19
|
+
|
20
|
+
attr_reader :credentials, :displayname
|
21
|
+
|
22
|
+
def initialize(credentials, parent, displayname, href, content_type)
|
23
|
+
@credentials, @parent, @href = credentials, parent, href
|
24
|
+
@content_type = CONTENT_TYPES[content_type] || Message
|
25
|
+
@displayname = displayname
|
26
|
+
end
|
27
|
+
|
28
|
+
# Used to access subfolders.
|
29
|
+
def method_missing(sym, *args)
|
30
|
+
|
31
|
+
if folders_hash.has_key?(sym.to_s)
|
32
|
+
folders_hash[sym.to_s]
|
33
|
+
else
|
34
|
+
puts folders_hash.keys.inspect
|
35
|
+
puts sym.to_s
|
36
|
+
raise FolderNotFoundError.new("#{sym} is not a subfolder of #{@displayname} - #{@href}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
include Enumerable
|
41
|
+
|
42
|
+
def message_hrefs(href_regex)
|
43
|
+
RExchange::MessageHref::find_message_hrefs(href_regex, @credentials, to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Iterate through each entry in this folder
|
47
|
+
def each
|
48
|
+
@content_type::find(@credentials, to_s).each do |item|
|
49
|
+
yield item
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Not Implemented!
|
54
|
+
def search(conditions = {})
|
55
|
+
raise NotImplementedError.new('Bad Touch!')
|
56
|
+
@content_type::find(@credentials, to_s, conditions)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return an Array of subfolders for this folder
|
60
|
+
def folders
|
61
|
+
@folders ||=
|
62
|
+
begin
|
63
|
+
request_body = <<-eos
|
64
|
+
<D:searchrequest xmlns:D = "DAV:">
|
65
|
+
<D:sql>
|
66
|
+
SELECT "DAV:displayname", "DAV:contentclass"
|
67
|
+
FROM SCOPE('shallow traversal of "#{@href}"')
|
68
|
+
WHERE "DAV:ishidden" = false
|
69
|
+
AND "DAV:isfolder" = true
|
70
|
+
</D:sql>
|
71
|
+
</D:searchrequest>
|
72
|
+
eos
|
73
|
+
folders = []
|
74
|
+
DavSearchRequest.execute(@credentials, :body => request_body) do |response|
|
75
|
+
|
76
|
+
|
77
|
+
# iterate through folders query and add a new Folder
|
78
|
+
# object for each, under a normalized name.
|
79
|
+
xpath_query = "/*/a:response[a:propstat/a:status/text() = 'HTTP/1.1 200 OK']"
|
80
|
+
Document.new(response.body).elements.each(xpath_query) do |m|
|
81
|
+
href = m.elements['a:href'].text
|
82
|
+
displayname = m.elements['a:propstat/a:prop/a:displayname'].text
|
83
|
+
contentclass = m.elements['a:propstat/a:prop/a:contentclass'].text
|
84
|
+
folders << Folder.new(@credentials, self, displayname, href, contentclass.split(':').last.sub(/folder$/, ''))
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
folders
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def folders_hash
|
94
|
+
@folders_hash ||= folders.inject({}){|memo, f| memo[f.displayname.normalize]=f; memo}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return the absolute path to this folder (but not the full URI)
|
98
|
+
def to_s
|
99
|
+
@href
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'rexchange/dav_move_request'
|
4
|
+
require 'rexchange/dav_get_request'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module RExchange
|
8
|
+
|
9
|
+
class Folder
|
10
|
+
CONTENT_TYPES = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
class GenericItem
|
14
|
+
include REXML
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
attr_accessor :attributes
|
18
|
+
|
19
|
+
def initialize(session, dav_property_node)
|
20
|
+
@attributes = {}
|
21
|
+
@session = session
|
22
|
+
|
23
|
+
dav_property_node.elements.each do |element|
|
24
|
+
namespaced_name = element.namespace + element.name
|
25
|
+
|
26
|
+
if element.name =~ /date$/i || self.class::ATTRIBUTE_MAPPINGS.find { |k, v| v == namespaced_name && k.to_s =~ /\_(at|on)$/ }
|
27
|
+
@attributes[namespaced_name] = Time::parse(element.text) rescue element.text
|
28
|
+
else
|
29
|
+
@attributes[namespaced_name] = element.text
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set the default CONTENT_CLASS to the class name, and define a
|
35
|
+
# dynamic query method for the derived class.
|
36
|
+
def self.inherited(base)
|
37
|
+
base.const_set('CONTENT_CLASS', base.to_s.split('::').last.downcase)
|
38
|
+
|
39
|
+
def base.query(path)
|
40
|
+
<<-QBODY
|
41
|
+
SELECT
|
42
|
+
#{self::ATTRIBUTE_MAPPINGS.values.map { |f| '"' + f + '"' }.join(',')}
|
43
|
+
FROM SCOPE('shallow traversal of "#{path}"')
|
44
|
+
WHERE "DAV:ishidden" = false
|
45
|
+
AND "DAV:isfolder" = false
|
46
|
+
AND "DAV:contentclass" = 'urn:content-classes:#{self::CONTENT_CLASS}'
|
47
|
+
QBODY
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This handy method is meant to be called from any inheriting
|
52
|
+
# classes. It is used to bind types of folders to particular
|
53
|
+
# Entity classes so that the folder knows what type it's
|
54
|
+
# enumerating. So for a "calendarfolder" you'd call:
|
55
|
+
# set_folder_type 'calendarfolder' # or just 'calendar'
|
56
|
+
def self.set_folder_type(dav_name)
|
57
|
+
Folder::CONTENT_TYPES[dav_name.sub(/folder$/, '')] = self
|
58
|
+
end
|
59
|
+
|
60
|
+
# --Normally Not Used--
|
61
|
+
# By default the CONTENT_CLASS is determined by the name
|
62
|
+
# of your class. So for the Appointment class the
|
63
|
+
# CONTENT_CLASS would be 'appointment'.
|
64
|
+
# If for some reason this convention doesn't suit you,
|
65
|
+
# you can use this method to set the appropriate value
|
66
|
+
# (which is used in queries).
|
67
|
+
# For example, the DAV:content-class for contacts is:
|
68
|
+
# 'urn:content-classes:person'
|
69
|
+
# Person doesn't strike me as the best name for our class though.
|
70
|
+
# Most people would refer to an entry in a Contacts folder as
|
71
|
+
# a Contact. So that's what we call our class, and we use this method
|
72
|
+
# to make sure everything still works as it should.
|
73
|
+
def self.set_content_class(dav_name)
|
74
|
+
verbosity, $VERBOSE = $VERBOSE, nil # disable warnings for the next operation
|
75
|
+
const_set('CONTENT_CLASS', dav_name)
|
76
|
+
$VERBOSE = verbosity # revert to the original verbosity
|
77
|
+
end
|
78
|
+
|
79
|
+
# Defines what attributes are used in queries, and
|
80
|
+
# what methods they map to in instances. You should
|
81
|
+
# pass a Hash of method_name symbols and namespaced-attribute-name pairs.
|
82
|
+
def self.attribute_mappings(mappings)
|
83
|
+
|
84
|
+
mappings.merge! :uid => 'DAV:uid',
|
85
|
+
:modified_at => 'DAV:getlastmodified',
|
86
|
+
:href => 'DAV:href'
|
87
|
+
|
88
|
+
const_set('ATTRIBUTE_MAPPINGS', mappings)
|
89
|
+
|
90
|
+
mappings.each_pair do |k, v|
|
91
|
+
|
92
|
+
define_method(k) do
|
93
|
+
@attributes[v]
|
94
|
+
end
|
95
|
+
|
96
|
+
define_method("#{k.to_s.sub(/\?$/, '')}=") do |value|
|
97
|
+
@attributes[v] = value
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Retrieve an Array of items (such as Contact, Message, etc)
|
104
|
+
def self.find(credentials, path, conditions = nil)
|
105
|
+
qbody = <<-QBODY
|
106
|
+
<D:searchrequest xmlns:D = "DAV:">
|
107
|
+
<D:sql>
|
108
|
+
#{conditions.nil? ? query(path) : search(path, conditions)}
|
109
|
+
</D:sql>
|
110
|
+
</D:searchrequest>
|
111
|
+
QBODY
|
112
|
+
items = []
|
113
|
+
DavSearchRequest.execute(credentials, :body => qbody) do |response|
|
114
|
+
|
115
|
+
xpath_query = "//a:propstat[a:status/text() = 'HTTP/1.1 200 OK']/a:prop"
|
116
|
+
|
117
|
+
Document.new(response.body).elements.each(xpath_query) do |m|
|
118
|
+
items << self.new(credentials, m)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
items
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rexchange/generic_item'
|
2
|
+
require 'rexchange/dav_proppatch_request'
|
3
|
+
|
4
|
+
module RExchange
|
5
|
+
class Message < GenericItem
|
6
|
+
|
7
|
+
set_folder_type 'mail'
|
8
|
+
|
9
|
+
attribute_mappings :from => 'urn:schemas:httpmail:from',
|
10
|
+
:to => 'urn:schemas:httpmail:to',
|
11
|
+
:message_id => 'urn:schemas:mailheader:message-id',
|
12
|
+
:subject => 'urn:schemas:httpmail:subject',
|
13
|
+
:recieved_on => 'urn:schemas:httpmail:date',
|
14
|
+
:importance => 'urn:schemas:httpmail:importance',
|
15
|
+
:has_attachments? => 'urn:schemas:httpmail:hasattachment',
|
16
|
+
:body => 'urn:schemas:httpmail:textdescription',
|
17
|
+
:html => 'urn:schemas:httpmail:htmldescription'
|
18
|
+
|
19
|
+
|
20
|
+
# Move this message to the specified folder.
|
21
|
+
# The folder can be a string such as 'inbox/archive' or a RExchange::Folder.
|
22
|
+
# === Example
|
23
|
+
# mailbox.inbox.each do |message|
|
24
|
+
# message.move_to mailbox.inbox.archive
|
25
|
+
# end
|
26
|
+
def move_to(folder)
|
27
|
+
destination =
|
28
|
+
if folder.is_a?(RExchange::Folder)
|
29
|
+
folder.to_s.ensure_ends_with('/') + self.href.split('/').last
|
30
|
+
else
|
31
|
+
@session.uri.path.ensure_ends_with('/') + folder.to_s.ensure_ends_with('/') + self.href.split('/').last
|
32
|
+
end
|
33
|
+
|
34
|
+
DavMoveRequest.execute(@session, self.href, destination)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Delete this message.
|
38
|
+
# === Example
|
39
|
+
# mailbox.inbox.each do |message|
|
40
|
+
# message.delete!
|
41
|
+
# end
|
42
|
+
def delete!
|
43
|
+
response = DavDeleteRequest.execute(@session, self.href)
|
44
|
+
case response.code
|
45
|
+
when 204 then
|
46
|
+
# Standard success response. ( http://msdn.microsoft.com/en-us/library/aa142839(EXCHG.65).aspx )
|
47
|
+
true
|
48
|
+
when 423 then
|
49
|
+
# The destination resource is locked.
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
"To: #{to}, From: #{from}, Subject: #{subject}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def raw
|
60
|
+
fetch(href, limit = 10)
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch(uri_str, limit = 10)
|
64
|
+
# You should choose better exception.
|
65
|
+
raise ArgumentError, 'HTTP redirect too deep' if limit == 0
|
66
|
+
response = DavGetRequest.execute(@session, uri_str)
|
67
|
+
case response
|
68
|
+
when Net::HTTPSuccess then
|
69
|
+
response.body
|
70
|
+
when Net::HTTPRedirection then
|
71
|
+
fetch(response['location'], limit - 1)
|
72
|
+
else
|
73
|
+
response.error!
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def mark_as_read
|
78
|
+
response = DavProppatchRequest.execute(@session, self.href, RExchange::PR_HTTPMAIL_READ, RExchange::NS_HTTPMAIL, 1)
|
79
|
+
case response
|
80
|
+
when Net::HTTPSuccess then
|
81
|
+
true
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rexchange/generic_item'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
class Note < GenericItem
|
5
|
+
|
6
|
+
set_folder_type 'note'
|
7
|
+
|
8
|
+
attribute_mappings :displayname => 'DAV:displayname',
|
9
|
+
:created_at => 'DAV:creationdate',
|
10
|
+
:subject =>'urn:schemas:httpmail:subject',
|
11
|
+
:body => 'urn:schemas:httpmail:textdescription'
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rexchange/folder'
|
3
|
+
require 'rexchange/credentials'
|
4
|
+
|
5
|
+
module RExchange
|
6
|
+
|
7
|
+
class Session < Folder
|
8
|
+
|
9
|
+
# Creates a Credentials instance to pass to subfolders
|
10
|
+
# === Example
|
11
|
+
# RExchange::Session.new('https://mydomain.com/exchange/demo', 'https://mydomain.com/owa/auth/owaauth.dll', 'mydomain\\bob', 'secret') do |mailbox|
|
12
|
+
# mailbox.test.each do |message|
|
13
|
+
# puts message.subject
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
def initialize(dav_uri, owa_uri, username = nil, password = nil)
|
17
|
+
|
18
|
+
@credentials = Credentials.new(dav_uri, owa_uri, username, password)
|
19
|
+
@parent = @credentials.dav_uri.path
|
20
|
+
@folder = ''
|
21
|
+
@href = @credentials.dav_uri.to_s
|
22
|
+
|
23
|
+
yield(self) if block_given?
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rexchange/generic_item'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
class Task < GenericItem
|
5
|
+
|
6
|
+
set_folder_type 'task'
|
7
|
+
|
8
|
+
attribute_mappings :displayname => 'DAV:displayname',
|
9
|
+
:created_at => 'DAV:creationdate',
|
10
|
+
:subject =>'urn:schemas:httpmail:subject',
|
11
|
+
:body => 'urn:schemas:httpmail:textdescription',
|
12
|
+
:percent_complete =>'http://schemas.microsoft.com/exchange/tasks/percentcomplete',
|
13
|
+
:owner =>'http://schemas.microsoft.com/exchange/tasks/owner',
|
14
|
+
:is_complete =>'http://schemas.microsoft.com/exchange/tasks/is_complete',
|
15
|
+
:date_start =>'http://schemas.microsoft.com/exchange/tasks/dtstart',
|
16
|
+
:date_due =>'http://schemas.microsoft.com/exchange/tasks/dtdue',
|
17
|
+
:actual_effort =>'http://schemas.microsoft.com/exchange/tasks/actualeffort',
|
18
|
+
:estimated_effort =>'http://schemas.microsoft.com/exchange/tasks/estimatedeffort',
|
19
|
+
:priority =>'http://schemas.microsoft.com/mapi/priority',
|
20
|
+
:status =>'http://schemas.microsoft.com/exchange/tasks/status',
|
21
|
+
:state =>'http://schemas.microsoft.com/exchange/tasks/state'
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
data/test/functional.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rexchange'
|
3
|
+
|
4
|
+
class FunctionalTests < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@mailbox = RExchange::Session.new 'url', 'username', 'password'
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
@mailbox = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Ok, so it's not a real test, but I needed to get started,
|
15
|
+
# and get rid of console scripts.
|
16
|
+
def test_no_exceptions
|
17
|
+
|
18
|
+
@mailbox.inbox.search(:from => 'scott').each do |m|
|
19
|
+
puts m.subject
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lambder-rexchange
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Smoot
|
8
|
+
- Scott Bauer
|
9
|
+
- Daniel Kwiecinski
|
10
|
+
autorequire: rexchange
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2009-08-27 00:00:00 -07:00
|
15
|
+
default_executable:
|
16
|
+
dependencies: []
|
17
|
+
|
18
|
+
description: Connect, browse, and iterate through folders and messages on an Exchange Server
|
19
|
+
email: ssmoot@gmail.com; bauer.mail@gmail.com; daniel@lambder.com
|
20
|
+
executables: []
|
21
|
+
|
22
|
+
extensions: []
|
23
|
+
|
24
|
+
extra_rdoc_files:
|
25
|
+
- README
|
26
|
+
- CHANGELOG
|
27
|
+
- RAKEFILE
|
28
|
+
- MIT-LICENSE
|
29
|
+
files:
|
30
|
+
- README
|
31
|
+
- CHANGELOG
|
32
|
+
- RAKEFILE
|
33
|
+
- MIT-LICENSE
|
34
|
+
- lib/r_exchange.rb
|
35
|
+
- lib/rexchange/appointment.rb
|
36
|
+
- lib/rexchange/contact.rb
|
37
|
+
- lib/rexchange/credentials.rb
|
38
|
+
- lib/rexchange/dav_get_request.rb
|
39
|
+
- lib/rexchange/dav_move_request.rb
|
40
|
+
- lib/rexchange/dav_search_request.rb
|
41
|
+
- lib/rexchange/exchange_request.rb
|
42
|
+
- lib/rexchange/folder.rb
|
43
|
+
- lib/rexchange/generic_item.rb
|
44
|
+
- lib/rexchange/message.rb
|
45
|
+
- lib/rexchange/note.rb
|
46
|
+
- lib/rexchange/session.rb
|
47
|
+
- lib/rexchange/task.rb
|
48
|
+
- lib/rexchange.rb
|
49
|
+
- test/functional.rb
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://substantiality.net
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options:
|
54
|
+
- --line-numbers
|
55
|
+
- --inline-source
|
56
|
+
- --main
|
57
|
+
- README
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
requirements:
|
73
|
+
- none
|
74
|
+
rubyforge_project: rexchange
|
75
|
+
rubygems_version: 1.2.0
|
76
|
+
signing_key:
|
77
|
+
specification_version: 2
|
78
|
+
summary: A simple wrapper around Microsoft Exchange Server's WebDAV API (that works with Exchange 2007 using basic authentication and the form-based one.
|
79
|
+
test_files: []
|
80
|
+
|