sonar_rexchange 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/RAKEFILE +50 -0
- data/README +89 -0
- data/REXCHANGE-CHANGELOG +52 -0
- data/REXCHANGE-MIT-LICENSE +22 -0
- data/VERSION +1 -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_delete_request.rb +30 -0
- data/lib/rexchange/dav_get_request.rb +28 -0
- data/lib/rexchange/dav_mkcol_request.rb +27 -0
- data/lib/rexchange/dav_move_request.rb +30 -0
- data/lib/rexchange/dav_proppatch_request.rb +49 -0
- data/lib/rexchange/dav_search_request.rb +8 -0
- data/lib/rexchange/exchange_request.rb +86 -0
- data/lib/rexchange/folder.rb +108 -0
- data/lib/rexchange/generic_item.rb +124 -0
- data/lib/rexchange/message.rb +88 -0
- data/lib/rexchange/message_href.rb +107 -0
- data/lib/rexchange/note.rb +16 -0
- data/lib/rexchange/r_exception.rb +25 -0
- data/lib/rexchange/session.rb +28 -0
- data/lib/rexchange/task.rb +26 -0
- data/test/functional.rb +23 -0
- metadata +92 -0
@@ -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,27 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used to make a new collection (aka folder).
|
5
|
+
class DavMkcolRequest < ExchangeRequest
|
6
|
+
METHOD = 'MKCOL'
|
7
|
+
REQUEST_HAS_BODY = false
|
8
|
+
RESPONSE_HAS_BODY = false
|
9
|
+
|
10
|
+
def self.execute(credentials, folder_url, &b)
|
11
|
+
begin
|
12
|
+
options = {
|
13
|
+
:path => folder_url
|
14
|
+
}
|
15
|
+
|
16
|
+
response = super credentials, options
|
17
|
+
yield response if b
|
18
|
+
response
|
19
|
+
rescue RException => e
|
20
|
+
raise e
|
21
|
+
rescue Exception => e
|
22
|
+
raise RException.new(options[:request], response, e)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
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,49 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used to move entities to different locations in the accessable mailbox.
|
5
|
+
class DavProppatchRequest < ExchangeRequest
|
6
|
+
METHOD = 'PROPPATCH'
|
7
|
+
|
8
|
+
|
9
|
+
def self.request_body(property, name_space, value)
|
10
|
+
<<-eos
|
11
|
+
<D:propertyupdate xmlns:D = "DAV:">
|
12
|
+
<D:set>
|
13
|
+
<D:prop>
|
14
|
+
<X:#{property} xmlns:X = "#{name_space}:">#{value}</X:read>
|
15
|
+
</D:prop>
|
16
|
+
</D:set>
|
17
|
+
</D:propertyupdate>
|
18
|
+
eos
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.execute(credentials, uri, property, name_space, value, &b)
|
22
|
+
begin
|
23
|
+
options = {}
|
24
|
+
options[:path] = uri
|
25
|
+
headers = options[:headers] ||= {}
|
26
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
27
|
+
options[:body] = request_body(property, name_space, value)
|
28
|
+
|
29
|
+
response = self.exchange_request(credentials, options)
|
30
|
+
case response
|
31
|
+
when Net::HTTPClientError then
|
32
|
+
self.authenticate(credentials)
|
33
|
+
#repeat exchange_request after authentication with the auth-cookies
|
34
|
+
headers = options[:headers] ||= {}
|
35
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
36
|
+
response = self.exchange_request(credentials, options)
|
37
|
+
end
|
38
|
+
yield response if b
|
39
|
+
response
|
40
|
+
rescue RException => e
|
41
|
+
raise e
|
42
|
+
rescue Exception => e
|
43
|
+
raise RException.new(options[:request], response, e)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
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,108 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'net/https'
|
3
|
+
require 'rexchange/dav_search_request'
|
4
|
+
require 'rexchange/dav_mkcol_request'
|
5
|
+
require 'rexchange/message'
|
6
|
+
require 'rexchange/contact'
|
7
|
+
require 'rexchange/appointment'
|
8
|
+
require 'rexchange/note'
|
9
|
+
require 'rexchange/task'
|
10
|
+
require 'rexchange/message_href'
|
11
|
+
require 'uri'
|
12
|
+
|
13
|
+
module RExchange
|
14
|
+
|
15
|
+
class FolderNotFoundError < StandardError
|
16
|
+
end
|
17
|
+
|
18
|
+
class Folder
|
19
|
+
include REXML
|
20
|
+
|
21
|
+
attr_reader :credentials, :displayname
|
22
|
+
|
23
|
+
def initialize(credentials, parent, displayname, href, content_type)
|
24
|
+
@credentials, @parent, @href = credentials, parent, href
|
25
|
+
@content_type = CONTENT_TYPES[content_type] || Message
|
26
|
+
@displayname = displayname
|
27
|
+
end
|
28
|
+
|
29
|
+
# Used to access subfolders.
|
30
|
+
def method_missing(sym, *args)
|
31
|
+
|
32
|
+
if folders_hash.has_key?(sym.to_s)
|
33
|
+
folders_hash[sym.to_s]
|
34
|
+
else
|
35
|
+
puts folders_hash.keys.inspect
|
36
|
+
puts sym.to_s
|
37
|
+
raise FolderNotFoundError.new("#{sym} is not a subfolder of #{@displayname} - #{@href}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
include Enumerable
|
42
|
+
|
43
|
+
def message_hrefs(href_regex)
|
44
|
+
RExchange::MessageHref::find_message_hrefs(href_regex, @credentials, to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Iterate through each entry in this folder
|
48
|
+
def each
|
49
|
+
@content_type::find(@credentials, to_s).each do |item|
|
50
|
+
yield item
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Not Implemented!
|
55
|
+
def search(conditions = {})
|
56
|
+
raise NotImplementedError.new('Bad Touch!')
|
57
|
+
@content_type::find(@credentials, to_s, conditions)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return an Array of subfolders for this folder
|
61
|
+
def folders
|
62
|
+
@folders ||=
|
63
|
+
begin
|
64
|
+
request_body = <<-eos
|
65
|
+
<D:searchrequest xmlns:D = "DAV:">
|
66
|
+
<D:sql>
|
67
|
+
SELECT "DAV:displayname", "DAV:contentclass"
|
68
|
+
FROM SCOPE('shallow traversal of "#{@href}"')
|
69
|
+
WHERE "DAV:ishidden" = false
|
70
|
+
AND "DAV:isfolder" = true
|
71
|
+
</D:sql>
|
72
|
+
</D:searchrequest>
|
73
|
+
eos
|
74
|
+
folders = []
|
75
|
+
DavSearchRequest.execute(@credentials, :body => request_body) do |response|
|
76
|
+
|
77
|
+
|
78
|
+
# iterate through folders query and add a new Folder
|
79
|
+
# object for each, under a normalized name.
|
80
|
+
xpath_query = "/*/a:response[a:propstat/a:status/text() = 'HTTP/1.1 200 OK']"
|
81
|
+
Document.new(response.body).elements.each(xpath_query) do |m|
|
82
|
+
href = m.elements['a:href'].text
|
83
|
+
displayname = m.elements['a:propstat/a:prop/a:displayname'].text
|
84
|
+
contentclass = m.elements['a:propstat/a:prop/a:contentclass'].text
|
85
|
+
folders << Folder.new(@credentials, self, displayname, href, contentclass.split(':').last.sub(/folder$/, ''))
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
folders
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def folders_hash
|
95
|
+
@folders_hash ||= folders.inject({}){|memo, f| memo[f.displayname.normalize]=f; memo}
|
96
|
+
end
|
97
|
+
|
98
|
+
def make_subfolder(subfolder)
|
99
|
+
path = @href.ensure_ends_with("/") + subfolder.ensure_ends_with("/")
|
100
|
+
DavMkcolRequest.execute(@credentials, path)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return the absolute path to this folder (but not the full URI)
|
104
|
+
def to_s
|
105
|
+
@href
|
106
|
+
end
|
107
|
+
end
|
108
|
+
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
|